@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 { hasHttpStatus, ToolError, BRANCH_INPUT, toolDef, CAN_WRITE_CLAUSE, defaultBranchWriteVerdicts } from '../tool-helpers/index.js';
5
5
  import { notifyAgentRead, assertAgentWriteAllowed, SESSION_ID_INPUT, } from './agent-access.gate.js';
@@ -8,7 +8,7 @@ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
8
8
  import { GitInternalsError, WorkflowValidationError } from '../../shared/domain-errors.js';
9
9
  // Leaf-level shared primitive (same exception `workspace.service.ts` already
10
10
  // relies on) — not a workflow service, so this stays inside the module boundary.
11
- import { assertInsideRepo, assertRepoRootNameFree, assertRepoRootNameFreeArgs, isInsideRepo, normalizePathArgs, normalizeWorkspacePath, } from '../kb-fs/index.js';
11
+ import { assertInsideRepo, assertRepoRootNameFree, assertRepoRootNameFreeArgs, hasControlCharacter, isInsideRepo, isTmpRef, normalizePathArgs, normalizeWorkspacePath, } from '../kb-fs/index.js';
12
12
  import { GitGuardedFilesystem, MoveLockedError, MoveRacedError } from '../kb-fs/index.js';
13
13
  import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
14
14
  import { isRolesYamlPath, toKbRelative, resolveReadableMap, AccessDeniedError } from '../access-model/index.js';
@@ -18,11 +18,13 @@ import { displayPath } from './file-readers/file-reader.js';
18
18
  import { fileTypeOf, needsContent } from './file-readers/content-mode.js';
19
19
  import { createFileReaderRegistry } from './file-readers/file-reader.registry.js';
20
20
  import { DocumentReader } from './file-readers/document-reader.js';
21
+ import { TextReader } from './file-readers/text-reader.js';
21
22
  import { mcpImageResult } from '@bevel-software/platform-mcp-core';
22
23
  import { folderPlaceholderPath, isFolderPlaceholder, isPlatformFile, isPlatformFolder, platformFileCreationRefusal, platformFileRefusal, platformFileUploadRefusal, platformFolderRefusal, isRepositoryOwnFile, repositoryOwnFileDeleteRefusal, entryExistsMessage, } from '@bevel-software/platform-shared';
23
24
  import { AGENT_GUIDE_FILE, isAgentGuidePath, isManagedGuide, withPlatformGuideAppended, } from '../agent-guide/index.js';
24
25
  import { removeEmptyDirs } from './empty-dirs.js';
25
- import { endsTheSearch } from './pattern-matcher.contract.js';
26
+ import { compareSearchPaths, PATTERN_DIALECT, } from './pattern-matcher.contract.js';
27
+ import { holdsNul } from './nul-scan.js';
26
28
  import { FIRST_RUN_SECTION_ID, firstRunNote, knowledgeFolderIsNew } from './first-run.js';
27
29
  import { planMoveLinks } from './move-links.js';
28
30
  import { rethrowAsWriteDenial } from './write-denial.js';
@@ -36,6 +38,12 @@ import { isSymlinkZipEntry, isZipNoiseEntry, readZipEntry, zipEntryName, zipEntr
36
38
  const log = logger('workspace-tools');
37
39
  /** Types that run scripts wherever they are opened, sent by `request_file_download` as plain bytes. */
38
40
  const ACTIVE_CONTENT_TYPES = new Set(['image/svg+xml', 'text/html', 'application/xhtml+xml']);
41
+ /**
42
+ * The caller's verdicts on a file you staged in the workspace's tmp folder: it
43
+ * may be read (and copied out), nothing else — the folder is read-only
44
+ * through the ref, and `request_file_download` does not take one.
45
+ */
46
+ const TMP_ACCESS = { read: true, write: false, download: false, owner: false };
39
47
  /** How many files `file_stat` counts under a folder before it stops and says so. */
40
48
  const DESCENDANTS_CAP = 10_000;
41
49
  /** How many of a folder's files a `delete_folder` answer names. */
@@ -67,13 +75,19 @@ async function filterReadableEntries(gate, dir, entries) {
67
75
  }
68
76
  if (fileWsPaths.length === 0)
69
77
  return entries;
70
- const verdict = await resolveReadableMap((wid, email, rels) => gate.accessControl.canReadBatch(wid, email, rels), gate.workspaceId, gate.userEmail, gate.kbDirName, fileWsPaths);
71
- return entries.filter((e) => {
72
- if (e.type === 'directory')
73
- return true;
74
- const wsPath = dir ? `${dir}/${e.name}` : e.name;
75
- return verdict.get(wsPath) === true;
76
- });
78
+ const readable = await readableFiles(gate, fileWsPaths);
79
+ return entries.filter((e) => e.type === 'directory' || readable.has(dir ? `${dir}/${e.name}` : e.name));
80
+ }
81
+ /**
82
+ * The FILES among `wsPaths` the caller may read, judged in one batched ACL
83
+ * load by the FULL `canReadBatch` (per-node frontmatter honoured), via the
84
+ * shared `resolveReadableMap`. A path the verdict does not name is unreadable.
85
+ */
86
+ async function readableFiles(gate, wsPaths) {
87
+ if (wsPaths.length === 0)
88
+ return new Set();
89
+ const verdict = await resolveReadableMap((wid, email, rels) => gate.accessControl.canReadBatch(wid, email, rels), gate.workspaceId, gate.userEmail, gate.kbDirName, wsPaths);
90
+ return new Set(wsPaths.filter((p) => verdict.get(p) === true));
77
91
  }
78
92
  /**
79
93
  * Drop the empty-folder placeholder from a directory listing: it keeps a
@@ -128,6 +142,12 @@ async function keepFolderOf(fs, ctx, branch, removedPath, kbDirName) {
128
142
  }
129
143
  const int = (description) => ({ type: 'integer', description });
130
144
  const str = (description) => ({ type: 'string', description });
145
+ /**
146
+ * A write to the default branch is live at once, so the agent asks first. Said on the four
147
+ * tools behind 87 of the 91 writes there in the agent-guide benchmark (2026-10-26); `start-here`
148
+ * says it for the other write tools.
149
+ */
150
+ const ASK_BEFORE_DEFAULT_BRANCH = ' 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.';
131
151
  /**
132
152
  * The upload route, named on every tool that takes content as a JSON string.
133
153
  *
@@ -180,6 +200,30 @@ const RESERVED_ROOT_NAME_TARGETS = {
180
200
  // inside the handler — an archive chooses its entry names, not the caller.
181
201
  apply_file_upload: ['destination'],
182
202
  };
203
+ /**
204
+ * The inputs that take a tmp ref (`__tmp__/<path>`, see `workspace-tmp.ts`)
205
+ * beside a repository path — the four reads of the one file it names. Every
206
+ * other tool, and every other input of these four (`copy_file`'s `dest`
207
+ * among them), refuses a ref before anything else runs — in
208
+ * `normalizePathArgs`, through the one admission gate (`kb-fs/tmp-ref.ts`):
209
+ * the folder is read-only through the ref, and `copy_file` to a repository
210
+ * path is how a file is kept.
211
+ */
212
+ const TMP_REF_INPUTS = {
213
+ read_file: ['path'],
214
+ file_stat: ['path'],
215
+ grep: ['path'],
216
+ copy_file: ['src'],
217
+ };
218
+ /**
219
+ * A path input that also takes a tmp ref (see `TMP_REF_INPUTS`): said on the
220
+ * input, because the descriptions of the tools that take one have no room
221
+ * left, and what the ref is is in the shared rules.
222
+ */
223
+ const orTmpRef = (schema) => ({
224
+ ...schema,
225
+ description: `${schema.description ?? ''} Or a \`__tmp__/…\` ref to one file you staged in the workspace's tmp folder.`,
226
+ });
183
227
  function asText(content) {
184
228
  return typeof content === 'string' ? content : content.toString('utf8');
185
229
  }
@@ -467,35 +511,49 @@ function modeOf(a) {
467
511
  return a.mode;
468
512
  throw new ToolError(`"${String(a.mode)}" is not a write mode: use ${WRITE_MODES.map((m) => `\`${m}\``).join(', ')} (default \`create\`).`, 400, { code: 'bad_mode' });
469
513
  }
514
+ /** The longest stretch of a matching line a grep answer carries. */
515
+ const GREP_LINE_CHARS = 300;
516
+ /** {@link searchableText}'s `bytesOf` for a repository path: the workspace filesystem's own read. */
517
+ function fileBytes(fs) {
518
+ return async (path) => asBytes(await fs.readFile(path));
519
+ }
470
520
  /**
471
- * Search ONE file and append its matches to `out`. Shared by the directory
472
- * walk and by a grep whose `path` names a file, so both produce the same match
473
- * shape (that path, 1-based line numbers, 300-char text) under the same cap.
474
- *
475
- * What is searched is the file's reader's business: text content for the text
476
- * reader (null on NUL bytes / invalid UTF-8 — binary is not searchable), the
477
- * EXTRACTION for a document reader (marker line included, so the
478
- * `[slide N]`/`[sheet: …]`/`[page N]` lines are themselves searchable and line
479
- * numbers match what read_file returns), nothing for images.
521
+ * Files the text pass judges, then searches, at a time: first this many, then
522
+ * twice as many each time, up to {@link GREP_MAX_BATCH}. A verdict reads the
523
+ * file's own access rules, so a batch costs one read of each of its files.
524
+ * Its names also go to ripgrep on the command line, so a batch also ends
525
+ * once they reach {@link GREP_BATCH_CHARS}, well inside the system's limit.
526
+ */
527
+ const GREP_FIRST_BATCH = 256;
528
+ const GREP_MAX_BATCH = 2_048;
529
+ const GREP_BATCH_CHARS = 256 * 1024;
530
+ /**
531
+ * The text ONE file holds for `grep`, read through its reader: the text
532
+ * content for a text reader (none on NUL bytes / invalid UTF-8 — binary is not
533
+ * searchable), the EXTRACTION for a document reader (marker line included, so
534
+ * the `[slide N]`/`[sheet: …]`/`[page N]` lines are themselves searchable and
535
+ * line numbers match what read_file returns), nothing for images.
480
536
  *
481
537
  * Throws whatever reading the file throws — the caller decides whether that is
482
- * a file to skip (the walk) or an error to surface (a single-file grep).
538
+ * a file to skip (a tree search) or an error to surface (a single-file grep).
539
+ * `bytesOf` is where the file's bytes come from: the workspace filesystem for
540
+ * a repository path, the tmp resolver for a tmp ref.
483
541
  */
484
- async function grepOneFile(fs, path, matcher, out, max, docs) {
542
+ async function searchableText(bytesOf, path, docs) {
485
543
  const reader = docs.readers.readerFor(path);
486
- const bytes = asBytes(await fs.readFile(path));
544
+ const bytes = await bytesOf(path);
487
545
  let content;
488
546
  if (reader instanceof DocumentReader) {
489
547
  // What the cache remembers is free — an extraction is searched, a failure
490
548
  // (a damaged file, or one that overran its budget within the hour) is
491
549
  // skipped without extracting it again. A cold document draws on the
492
- // per-walk budget (see UNCACHED_DOCS_PER_GREP) — beyond it the search
550
+ // per-call budget (see UNCACHED_DOCS_PER_GREP) — beyond it the search
493
551
  // skips the document and counts it.
494
552
  let res = await reader.cachedRead(bytes, path);
495
553
  if (res === undefined) {
496
554
  if (docs.uncachedBudget <= 0) {
497
555
  docs.skippedUncached++;
498
- return 'budget-skipped';
556
+ return { kind: 'budget-skipped' };
499
557
  }
500
558
  docs.uncachedBudget--;
501
559
  res = await reader.read(bytes, path, docs.origin);
@@ -506,15 +564,41 @@ async function grepOneFile(fs, path, matcher, out, max, docs) {
506
564
  else {
507
565
  content = reader.greppableText ? await reader.greppableText(bytes, path) : null;
508
566
  }
509
- if (content === null)
510
- return 'no-text';
511
- const lines = content.split('\n');
512
- // matchingLines posts these to the worker in bounded batches, so no one
513
- // copy of a large file runs on the request thread.
514
- for (const i of await matcher.matchingLines(lines, max - out.length)) {
515
- out.push({ path, line: i + 1, text: lines[i].slice(0, 300) });
567
+ return content === null ? { kind: 'no-text' } : { kind: 'text', text: content };
568
+ }
569
+ /**
570
+ * grep over the one text file `path`, by ripgrep from the folder holding it,
571
+ * its lines pushed onto `out`, when it is a regular file; `null`, with
572
+ * nothing searched, for anything else (a link, one that loops, a file gone
573
+ * since), which is read as read_file reads it instead. A file that answers no
574
+ * line has no text when ripgrep finds it binary: it holds a NUL, which
575
+ * {@link holdsNul} looks for in the very file that was judged.
576
+ */
577
+ async function grepTextFile(fs, path, search, max, out) {
578
+ const slash = path.lastIndexOf('/');
579
+ const scope = await treeScope(fs, slash < 0 ? '' : path.slice(0, slash));
580
+ const name = path.slice(slash + 1);
581
+ const absolute = join(scope.cwd, scope.root, name);
582
+ const judged = await nodeFs.lstat(absolute).catch(() => null);
583
+ if (judged === null || !judged.isFile())
584
+ return null;
585
+ const from = out.length;
586
+ for await (const file of search.files(scope, { only: [name], maxLinesPerFile: max })) {
587
+ for (const l of file.lines)
588
+ out.push({ path, line: l.line, text: l.text.slice(0, GREP_LINE_CHARS) });
516
589
  }
517
- return 'searched';
590
+ if (out.length > from)
591
+ return { kind: 'searched' };
592
+ const binary = await orNotFound(path, () => holdsNul(absolute, judged, search), 'Nothing to search');
593
+ return binary ? { kind: 'no-text' } : { kind: 'searched' };
594
+ }
595
+ /** Append the lines `hits` names (0-based indices into `text`'s lines) to `out`, as matches in `path`. */
596
+ function pushMatches(out, path, text, hits) {
597
+ if (hits.length === 0)
598
+ return;
599
+ const lines = text.split('\n');
600
+ for (const i of hits)
601
+ out.push({ path, line: i + 1, text: lines[i].slice(0, GREP_LINE_CHARS) });
518
602
  }
519
603
  /**
520
604
  * What a grep's `path` actually names. The walk starts with `readdir`, which
@@ -548,55 +632,145 @@ async function searchRootKind(fs, path) {
548
632
  return 'file';
549
633
  }
550
634
  }
551
- /** JS grep over the workspace tree (read methods only) — bounded by match + depth caps. */
552
- async function grepWalk(fs, dir, matcher, out, max, depth, gate, notifyRead, docs,
553
- /** A file the walk leaves to its caller: never opened, never counted against `max`. */
554
- skip = () => false) {
555
- if (out.length >= max || depth > 12)
556
- return;
557
- let entries;
558
- try {
559
- entries = (await fs.readdir(dir || '.'));
560
- }
561
- catch {
562
- return;
635
+ /**
636
+ * Where ripgrep searches the directory `root`: from the workspace's canonical
637
+ * directory, with the root's canonical path relative to it. The stat that
638
+ * called `root` a directory already refused one outside the workspace; this
639
+ * refuses it again where the search is started, so ripgrep, which follows the
640
+ * root it is given, is never started on anything outside.
641
+ */
642
+ async function treeScope(fs, root) {
643
+ const cwd = await nodeFs.realpath(fs.basePath);
644
+ const absolute = fs.resolveAbsolutePath(root);
645
+ const rel = absolute === undefined ? '..' : relative(cwd, await nodeFs.realpath(absolute));
646
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
647
+ throw new ToolError(`"${displayPath(root)}" is outside the workspace.`, 403);
563
648
  }
564
- // Filter unreadable KB nodes out of the walk so grep never opens — or leaks
565
- // a line from — a file the caller may not read.
566
- entries = await filterReadableEntries(gate, dir, entries);
567
- for (const e of entries) {
568
- if (out.length >= max)
569
- return;
570
- if (hasGitInternalsSegment(e.name) || e.name === 'node_modules')
571
- continue;
572
- if (e.type !== 'directory' && isFolderPlaceholder(e.name))
573
- continue;
574
- const p = dir ? `${dir}/${e.name}` : e.name;
575
- if (e.type === 'directory') {
576
- await grepWalk(fs, p, matcher, out, max, depth + 1, gate, notifyRead, docs, skip);
649
+ return { cwd, root: rel === '' ? '.' : rel.split(sep).join('/') };
650
+ }
651
+ /**
652
+ * grep over the directory `root`: the tree is listed, then searched in two
653
+ * passes, and no file is opened before it is admitted.
654
+ *
655
+ * The listing opens no file. Admission is the ONE step a file passes before
656
+ * either pass opens it: it is judged readable by the gate, and then — being a
657
+ * file the search will read, as `read_file` reads one — it is told to the
658
+ * read hook, which may refuse it, and with it the search. A file the caller
659
+ * may not read is never opened and never told of (there is no read to tell):
660
+ * what it holds can change neither the answer nor how long the search takes,
661
+ * and so not whether it ends within its budget. Judging and telling are both
662
+ * spent from that budget. A file that was not in the listing (it appeared
663
+ * since) is never admitted, so never searched.
664
+ *
665
+ * The text pass is ripgrep's own: every file whose reader greps its bytes as
666
+ * text (a TextReader) is searched by it, in path order, and read nowhere else.
667
+ * It admits a batch at a time, before ripgrep opens any of it, so the hook
668
+ * hears of each file of a batch whether or not a line of it is answered —
669
+ * and, past the line that fills the cap, of the rest of that batch, which the
670
+ * pass then never opens. Each file is answered as ripgrep finishes it, and
671
+ * the pass stops at the `max`-th line.
672
+ *
673
+ * The document pass reads the files every other reader owns through that
674
+ * reader, as `read_file` does — extractions under the per-call budget — and
675
+ * has their text matched by the same engine. A document that sorts after the
676
+ * text pass's last line cannot be among the first `max`, so it is not read.
677
+ *
678
+ * Answers what both found, unsorted: the caller merges, sorts and caps.
679
+ */
680
+ async function grepTree(fs, root, search, max, gate, notifyRead, docs,
681
+ /** A file the search leaves to its caller: never answered, never counted against `max`. */
682
+ skip = () => false) {
683
+ const scope = await treeScope(fs, root);
684
+ // A folder named with a trailing slash answers the paths it is spelled
685
+ // with everywhere else (`a/b.md`, never `a//b.md`), so the guide's own
686
+ // path and every verdict meet the one spelling.
687
+ const base = root.replace(/\/+$/, '');
688
+ const at = (p) => (base === '' ? p : `${base}/${p}`);
689
+ // ripgrep already stays out of `.git`; the folder's every other spelling,
690
+ // and the folder placeholder, are left out as everywhere else.
691
+ const shown = (p) => !hasGitInternalsSegment(p) && !isFolderPlaceholder(p) && !skip(p);
692
+ // Which reader owns an extension is the registry's answer, asked of a name
693
+ // carrying it; a TextReader greps the bytes themselves, which is ripgrep's
694
+ // pass. Extensions any other reader owns are left out of it.
695
+ const readerOf = (ext) => docs.readers.readerFor(`file${ext}`);
696
+ const notRaw = docs.readers.ownedExtensions().filter((ext) => !(readerOf(ext) instanceof TextReader));
697
+ const ownedBy = (p, exts) => {
698
+ const lower = p.toLowerCase();
699
+ return exts.some((ext) => lower.endsWith(ext));
700
+ };
701
+ const readerOwned = notRaw.filter((ext) => {
702
+ const reader = readerOf(ext);
703
+ return reader instanceof DocumentReader || reader.greppableText !== undefined;
704
+ });
705
+ const out = [];
706
+ // Admission (see above): the gate's verdict on `paths`, then the hook told
707
+ // of each readable one in path order — a refusal ends the search here, as
708
+ // it ends a `read_file`, before anything is opened. Spent from the budget.
709
+ const admit = (paths) => search.charged(async () => {
710
+ const readable = await readableFiles(gate, paths);
711
+ for (const p of paths) {
712
+ if (readable.has(p))
713
+ await notifyRead(p);
577
714
  }
578
- else if (skip(p)) {
579
- continue;
715
+ return readable;
716
+ });
717
+ // Every file the walk covers, in path order. Only what is shown is ever
718
+ // admitted, so what is admitted is what may be answered. A name holding a
719
+ // control character is no file name to the platform — no tool reads or
720
+ // writes one — so it is not searched either.
721
+ const all = (await search.list(scope)).filter((p) => shown(at(p)) && !hasControlCharacter(p));
722
+ const raw = all.filter((p) => !ownedBy(p, notRaw));
723
+ // The text pass: a batch of files is admitted, then its admitted files, and
724
+ // only they, are searched. A batch is admitted before ripgrep opens any of
725
+ // it, and the pass stops at the `max`-th line, so the verdicts asked for are
726
+ // those of the files the answer reached — few for a common word — and the
727
+ // batches double so a rare one still walks the tree in a handful of runs.
728
+ let size = GREP_FIRST_BATCH;
729
+ for (let from = 0; from < raw.length && out.length < max; size = Math.min(size * 2, GREP_MAX_BATCH)) {
730
+ const batch = [];
731
+ for (let chars = 0; from < raw.length && batch.length < size && (batch.length === 0 || chars < GREP_BATCH_CHARS); from++) {
732
+ batch.push(raw[from]);
733
+ chars += raw[from].length;
580
734
  }
581
- else {
582
- // Opening a file is a read of it, even when the walk started at a root
583
- // the read hook was already told about — so every file the walk opens
584
- // reaches the hook by name (closes the read-leak).
585
- await notifyRead(p);
586
- // A file the walk cannot read is silently skipped: one unreadable entry
587
- // must not fail a search over the whole tree.
588
- // A pattern too expensive to finish, or no worker free to run it, is not
589
- // an unreadable file: either ends the whole search.
590
- try {
591
- await grepOneFile(fs, p, matcher, out, max, docs);
592
- }
593
- catch (err) {
594
- if (endsTheSearch(err))
595
- throw err;
735
+ const readable = await admit(batch.map(at));
736
+ const only = batch.filter((p) => readable.has(at(p)));
737
+ for await (const file of search.files(scope, { only, maxLinesPerFile: max - out.length })) {
738
+ const p = at(file.path);
739
+ // Only what was admitted answers, whatever ripgrep printed.
740
+ if (!readable.has(p))
596
741
  continue;
742
+ for (const l of file.lines.slice(0, max - out.length)) {
743
+ out.push({ path: p, line: l.line, text: l.text.slice(0, GREP_LINE_CHARS) });
597
744
  }
745
+ // Enough: leaving the loop kills ripgrep at the file that filled the cap.
746
+ if (out.length >= max)
747
+ break;
748
+ }
749
+ }
750
+ const last = out.length >= max ? out[out.length - 1].path : undefined;
751
+ const candidates = all
752
+ .filter((p) => ownedBy(p, readerOwned))
753
+ .map(at)
754
+ .filter((p) => last === undefined || compareSearchPaths(p, last) <= 0);
755
+ const readable = await admit(candidates);
756
+ const texts = [];
757
+ for (const p of candidates) {
758
+ if (!readable.has(p))
759
+ continue;
760
+ let found;
761
+ try {
762
+ found = await searchableText(fileBytes(fs), p, docs);
763
+ }
764
+ catch {
765
+ // One document that cannot be read must not fail a search over the tree.
766
+ continue;
598
767
  }
768
+ if (found.kind === 'text')
769
+ texts.push({ path: p, text: found.text });
599
770
  }
771
+ const hits = await search.matchingLines(texts.map((t) => t.text));
772
+ texts.forEach((t, i) => pushMatches(out, t.path, t.text, hits[i]));
773
+ return out;
600
774
  }
601
775
  const SAVE_WARNINGS_OUTPUT = {
602
776
  type: 'array',
@@ -661,8 +835,9 @@ changeGate,
661
835
  */
662
836
  uploads,
663
837
  /**
664
- * The platform's agent guide (see `modules/agent-guide`), which `read_file`
665
- * and `file_stat` answer at the guide's name in the repository root — after
838
+ * The start of the platform's agent guide and the list of its sections
839
+ * (`guideStartText` in `modules/agent-guide`), which `read_file` and
840
+ * `file_stat` answer at the guide's name in the repository root — after
666
841
  * the knowledge base's own file of that name, when it has one. Optional for
667
842
  * the harnesses that are about the file primitives; without it the two
668
843
  * tools read the disk and nothing else.
@@ -842,7 +1017,9 @@ patternMatcher) {
842
1017
  const paths = toolPathArgs(args);
843
1018
  for (const p of paths)
844
1019
  assertNoGitInternalsSegment(p);
845
- const onDisk = paths.filter((p) => !spillStore.isSpillRef(p));
1020
+ // A spill ref belongs to no workspace, and a tmp ref to the folder beside
1021
+ // the checkout: neither is judged against the checkout's git folder.
1022
+ const onDisk = paths.filter((p) => !spillStore.isSpillRef(p) && !isTmpRef(p));
846
1023
  if (onDisk.length === 0)
847
1024
  return;
848
1025
  const root = await gitCheckRootFor(args, ctx);
@@ -896,9 +1073,14 @@ patternMatcher) {
896
1073
  * caller WILL have, never whether they may do it. The write verdicts that
897
1074
  * gate the move are `writeBlocked` and the lock gate, both of which read
898
1075
  * the tree as it is.
1076
+ *
1077
+ * `sourceContent` is the text of a source that is no repository path (a
1078
+ * tmp ref): it carries rules in exactly as a repository file would, so it
1079
+ * is previewed through the same relocation reading, never as "nothing
1080
+ * travels".
899
1081
  */
900
1082
  const accessAfter = async (branch, ctx, src, dest, opts) => {
901
- const from = toKbRelative(src, kbDirName);
1083
+ const from = opts?.sourceContent !== undefined ? src : toKbRelative(src, kbDirName);
902
1084
  const to = toKbRelative(dest, kbDirName);
903
1085
  // Outside the repository there are no rules to carry, and none to land
904
1086
  // among — the same answer `accessAt` gives for such a path.
@@ -906,6 +1088,16 @@ patternMatcher) {
906
1088
  return accessAt(branch, ctx, dest);
907
1089
  return accessControl.previewAccessAfterRelocation(workspaceIdForBranch(branch), ctx.user.email, from, to, opts);
908
1090
  };
1091
+ /**
1092
+ * The text a tmp source carries into the repository, for the preview only:
1093
+ * read as a repository source is read there — a file that cannot be read
1094
+ * carries no rules — so the preview never answers before the write
1095
+ * verdict. Whether the file is there at all is `statTmpFile`'s to say.
1096
+ */
1097
+ const tmpSourceText = (branch, ctx, ref) => ctx.workspaceService
1098
+ .readTmpFile(workspaceIdForBranch(branch), ctx.user.id, ref)
1099
+ .then((bytes) => bytes.toString('utf-8'))
1100
+ .catch(() => '');
909
1101
  /**
910
1102
  * What `copy_file`'s dry run answers: the same impact shape `move_file`
911
1103
  * previews, over a copy's own rules.
@@ -933,10 +1125,16 @@ patternMatcher) {
933
1125
  * them nothing they could not ask `file_stat` for.
934
1126
  */
935
1127
  const copyImpact = async (branch, ctx, src, dest) => {
936
- const [before, after, blocked] = await Promise.all([
937
- accessAt(branch, ctx, src),
938
- accessAfter(branch, ctx, src, dest, { sourceRemains: true }),
939
- writeBlocked(branch, ctx, [dest]),
1128
+ const fromTmp = isTmpRef(src);
1129
+ // The write verdict FIRST, and alone: a staged file's bytes are read only
1130
+ // for a caller who may write the destination. A denied preview carries
1131
+ // no rules from the source ('' — what an unreadable source carries), so
1132
+ // its `access.after` is the destination's own rules for the caller.
1133
+ const blocked = await writeBlocked(branch, ctx, [dest]);
1134
+ const sourceContent = fromTmp ? (blocked.length > 0 ? '' : await tmpSourceText(branch, ctx, src)) : undefined;
1135
+ const [before, after] = await Promise.all([
1136
+ fromTmp ? TMP_ACCESS : accessAt(branch, ctx, src),
1137
+ accessAfter(branch, ctx, src, dest, { sourceRemains: true, sourceContent }),
940
1138
  ]);
941
1139
  const access = { before, after };
942
1140
  const accessChanges = verbsDiffer(before, after);
@@ -953,7 +1151,11 @@ patternMatcher) {
953
1151
  };
954
1152
  }
955
1153
  const fs = await ctx.getFilesystem(branch);
956
- const kind = await kindOf(fs, src);
1154
+ // A tmp ref names one file by rule: resolving it is the whole look, and
1155
+ // a folder or nothing there is refused by the resolver itself.
1156
+ const kind = fromTmp
1157
+ ? (await orNotFound(src, () => ctx.workspaceService.statTmpFile(workspaceIdForBranch(branch), ctx.user.id, src), 'Nothing to copy'), 'file')
1158
+ : await kindOf(fs, src);
957
1159
  if (kind === null)
958
1160
  throw notFound(src, 'Nothing to copy');
959
1161
  const srcFiles = kind === 'folder' ? (await filesUnder(fs, src)).files : [src];
@@ -978,6 +1180,39 @@ patternMatcher) {
978
1180
  copied: false,
979
1181
  };
980
1182
  };
1183
+ /**
1184
+ * `copy_file` out of the workspace's tmp folder: the bytes the ref names
1185
+ * (through the one tmp resolver), landed at `dest` by the ordinary write —
1186
+ * the lock, the pre-disk validation, the creator's read grant for a new
1187
+ * folder and the commit every write to the repository takes — and
1188
+ * exclusively, so a name taken since the handler's look is refused with
1189
+ * the same sentence rather than replaced. From then on the copy is a
1190
+ * repository file like any other; the staged file is left where it was.
1191
+ *
1192
+ * A missing source is the ordinary 404 naming the ref, asked only after
1193
+ * the write verdict (as for any copy); an absence met while landing is the
1194
+ * destination's.
1195
+ */
1196
+ const copyFromTmp = async (fs, branch, ctx, src, dest) => {
1197
+ const bytes = await orNotFound(src, () => ctx.workspaceService.readTmpFile(workspaceIdForBranch(branch), ctx.user.id, src), 'Nothing to copy');
1198
+ try {
1199
+ // Exclusive, under the destination's lock: a name taken since the look
1200
+ // is refused there as `DestinationTakenError` — releasing the lock
1201
+ // untouched, so the winner's queued file stays — and answered as a
1202
+ // repository copy's lost race is (`asEntryExists`).
1203
+ await asEntryExists(() => fs.writeFile(dest, bytes, { overwrite: false }));
1204
+ }
1205
+ catch (err) {
1206
+ if (err instanceof ToolError)
1207
+ throw err;
1208
+ const occupiedBy = await existingAt(await workspaceRoot(branch, ctx), dest);
1209
+ if (occupiedBy !== null)
1210
+ throw new ToolError(entryExistsMessage(occupiedBy, dest), 409);
1211
+ if (isAbsence(err))
1212
+ throw notFound(dest, 'Nowhere to copy to');
1213
+ throw err;
1214
+ }
1215
+ };
981
1216
  /**
982
1217
  * The paths among `paths` the caller may NOT write, judged exactly as the
983
1218
  * lock gate judges them (`WorkflowService.acquireLock`): on a protected
@@ -1342,11 +1577,21 @@ patternMatcher) {
1342
1577
  // spill ref; for any other, `__tool_chain_spill__/x` is an ordinary
1343
1578
  // path, and exempting it there would be a workspace-relative path that
1344
1579
  // never reached the repository — the whole bug, spelled with a prefix.
1580
+ //
1581
+ // The other is a tmp ref (`__tmp__/…`), left as written in exactly the
1582
+ // inputs `TMP_REF_INPUTS` names and refused in every other one first,
1583
+ // so no tool sees one it does not resolve itself.
1345
1584
  toolHandler(async (args, ctx) => {
1346
1585
  // BEFORE the normaliser: see `assertToolPathsNotGitInternals`.
1347
1586
  if (spec.fileTool !== false)
1348
1587
  await assertToolPathsNotGitInternals(args, ctx);
1349
- const normalized = normalizePathArgs(args, kbDirName, spec.name === 'read_file' ? (v) => spillStore.isSpillRef(v) : undefined);
1588
+ // The normaliser admits a tmp ref in exactly the inputs
1589
+ // `TMP_REF_INPUTS` names and refuses it in every other one first.
1590
+ const normalized = normalizePathArgs(args, kbDirName, {
1591
+ tool: spec.name,
1592
+ tmpRefInputs: TMP_REF_INPUTS[spec.name],
1593
+ skip: spec.name === 'read_file' ? (v) => spillStore.isSpillRef(v) : undefined,
1594
+ });
1350
1595
  // The checkout's own name is reserved at the repository root on this
1351
1596
  // surface too. The routes meet that rule inside `WorkspaceService`;
1352
1597
  // these tools write through the locking filesystem, which never
@@ -1398,13 +1643,13 @@ patternMatcher) {
1398
1643
  // transport hiccup on its first call as an unrecoverable start.
1399
1644
  const startSessionDef = toolDef({
1400
1645
  name: 'start_session',
1401
- description: '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.',
1646
+ description: '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.',
1402
1647
  path: '/api/agent/tools/start_session',
1403
1648
  inputs: { type: 'object', properties: {}, additionalProperties: false },
1404
1649
  outputs: {
1405
1650
  type: 'object',
1406
1651
  properties: {
1407
- sessionId: str('The minted session id — pass it as `sessionId` on subsequent KnowledgeBase tool calls and to `ask`.'),
1652
+ sessionId: str('The minted session id — pass it as `sessionId` to the tools that take one, and to `ask` where there is one.'),
1408
1653
  firstRun: str(`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).`),
1409
1654
  },
1410
1655
  required: ['sessionId'],
@@ -1497,6 +1742,61 @@ patternMatcher) {
1497
1742
  * copy drifts on the first change to any of them.
1498
1743
  */
1499
1744
  const readForTool = async (branch, p, ctx) => (await readServed(branch, p, ctx)).result;
1745
+ /**
1746
+ * What a tmp ref (see `workspace-tmp.ts`) reads as: the bytes of the one
1747
+ * file it names in the CALLER's own folder of the branch workspace's `tmp/`
1748
+ * — through `readTmpFile`, the resolver a distribution's tool reads it with
1749
+ * — put through the SAME reader a repository file of that name gets. No
1750
+ * access gate and no read hook: the folder is outside the repository, so
1751
+ * no rule governs it and no knowledge-base path is read; what keeps one
1752
+ * person's staged file from another is that the ref resolves inside the
1753
+ * caller's folder alone. A missing file — somebody else's included — is the
1754
+ * ordinary 404, naming the ref.
1755
+ */
1756
+ const readTmp = async (branch, ref, ctx) => {
1757
+ const workspaceId = workspaceIdForBranch(branch);
1758
+ const bytes = await orNotFound(ref, () => ctx.workspaceService.readTmpFile(workspaceId, ctx.user.id, ref));
1759
+ return readers.readerFor(ref).read(bytes, ref, { workspaceId });
1760
+ };
1761
+ /**
1762
+ * `file_stat` on a tmp ref: the shape a repository file's stat has, its
1763
+ * content fields decided by the same reader as {@link readTmp} reads with —
1764
+ * and nothing to DO with it but read and copy out. Not written, moved,
1765
+ * deleted or downloaded through these tools (`managed` is false: the
1766
+ * platform's own files are a repository notion), and so not text-editable
1767
+ * either, whatever its content.
1768
+ */
1769
+ const statTmp = async (branch, ref, ctx) => {
1770
+ const workspaceId = workspaceIdForBranch(branch);
1771
+ const stat = await orNotFound(ref, () => ctx.workspaceService.statTmpFile(workspaceId, ctx.user.id, ref));
1772
+ const reader = readers.readerFor(ref);
1773
+ // One read of the file at most: the type decision and the remembered
1774
+ // extraction below both want the bytes, and a staged document can be large.
1775
+ let bytes;
1776
+ const bytesOf = async () => (bytes ??= await orNotFound(ref, () => ctx.workspaceService.readTmpFile(workspaceId, ctx.user.id, ref)));
1777
+ const type = fileTypeOf(reader, ref, needsContent(reader) ? await bytesOf() : undefined);
1778
+ const out = {
1779
+ ...stat,
1780
+ path: ref,
1781
+ type: 'file',
1782
+ managed: false,
1783
+ movable: false,
1784
+ deletable: false,
1785
+ access: TMP_ACCESS,
1786
+ canWrite: false,
1787
+ ...type,
1788
+ textEditable: false,
1789
+ };
1790
+ // A document whose extraction failed answers that failure's notice, as a
1791
+ // repository document does: from what the cache remembers, never by
1792
+ // extracting.
1793
+ if (reader instanceof DocumentReader) {
1794
+ const remembered = await reader.cachedRead(await bytesOf(), ref);
1795
+ if (remembered?.kind === 'refusal')
1796
+ return { ...out, unreadable: remembered.message };
1797
+ }
1798
+ return out;
1799
+ };
1500
1800
  /**
1501
1801
  * {@link readForTool}, also saying, at the guide's name, whether a file of
1502
1802
  * the knowledge base's own answered first on THIS branch (`guide`: `own`)
@@ -1550,7 +1850,7 @@ patternMatcher) {
1550
1850
  description: 'Read a workspace file as text. Returns `{ path, content, canWrite }`. Per the shared content rule: text as ' +
1551
1851
  'text, documents and email as extracted text, an image as the picture, anything else as a one-line ' +
1552
1852
  'description. Also reads a `__tool_chain_spill__/…` ref back from a truncated `call_tool_chain`: it ' +
1553
- 'belongs to no workspace, so `branch` is ignored and no `canWrite` comes back. `offset`/`limit` slice the ' +
1853
+ 'belongs to no workspace: any existing `branch` will do, and no `canWrite` comes back. `offset`/`limit` slice the ' +
1554
1854
  'content (characters; bytes for a spill ref; ignored for an image): page through large files and ' +
1555
1855
  'spills, never read multi-MB whole. `canWrite`: may you change this file directly on the default branch. ' +
1556
1856
  CAN_WRITE_CLAUSE,
@@ -1558,9 +1858,9 @@ patternMatcher) {
1558
1858
  type: 'object',
1559
1859
  properties: {
1560
1860
  branch: BRANCH_INPUT,
1561
- 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\`.`),
1562
- offset: int('Start character index (default 0).'),
1563
- limit: int('Max characters to return from `offset`.'),
1861
+ 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.`),
1862
+ offset: int('Start character index (bytes for a spill ref; default 0).'),
1863
+ limit: int('Max characters to return from `offset` (bytes for a spill ref).'),
1564
1864
  sessionId: SESSION_ID_INPUT,
1565
1865
  },
1566
1866
  required: ['branch', 'path'],
@@ -1594,7 +1894,11 @@ patternMatcher) {
1594
1894
  : content;
1595
1895
  };
1596
1896
  const branch = a.branch;
1597
- const { result, guide } = await readServed(branch, p, ctx);
1897
+ // A tmp ref is no repository path, so the `canWrite` below is false for
1898
+ // it by the same rule as for any path outside the repository.
1899
+ const { result, guide } = isTmpRef(p)
1900
+ ? { result: await readTmp(branch, p, ctx), guide: null }
1901
+ : await readServed(branch, p, ctx);
1598
1902
  // Judged after the read, so a path the caller may not read is refused
1599
1903
  // before anything is said about writing it. At the guide's name this
1600
1904
  // answers for the knowledge base's own file there, as `file_stat` does:
@@ -1798,7 +2102,7 @@ patternMatcher) {
1798
2102
  type: 'object',
1799
2103
  properties: {
1800
2104
  branch: BRANCH_INPUT,
1801
- path: wsPath(kbDirName, 'Path'),
2105
+ path: orTmpRef(wsPath(kbDirName, 'Path')),
1802
2106
  explainAccess: {
1803
2107
  type: 'boolean',
1804
2108
  description: 'Also explain `access` (default false): `access.why` says what decided each of your verdicts — the folder rules or file frontmatter and whether that is inherited; `access.roster` lists who holds each verb and where each grant is written, given only when you can manage this path\'s access (otherwise null, with `access.rosterReason`).',
@@ -1871,6 +2175,8 @@ patternMatcher) {
1871
2175
  handler: async (a, ctx) => {
1872
2176
  const p = a.path;
1873
2177
  const branch = a.branch;
2178
+ if (isTmpRef(p))
2179
+ return statTmp(branch, p, ctx);
1874
2180
  // The guide's name with no file of the knowledge base's own under it —
1875
2181
  // or one the caller may not read, or a copy of the guide an earlier
1876
2182
  // release wrote, none of which `read_file` serves: what a read answers
@@ -2034,13 +2340,14 @@ patternMatcher) {
2034
2340
  mount({
2035
2341
  name: 'grep',
2036
2342
  gated: true,
2037
- description: '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).',
2343
+ description: `Regex search across the workspace. ${PATTERN_DIALECT} Covers hidden and git-ignored files. ` +
2344
+ '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).',
2038
2345
  inputs: {
2039
2346
  type: 'object',
2040
2347
  properties: {
2041
2348
  branch: BRANCH_INPUT,
2042
- pattern: str('JavaScript regular expression.'),
2043
- 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}/\`).`),
2349
+ pattern: str('Regular expression, RE2-style (ripgrep): no look-around, no backreferences.'),
2350
+ 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.`),
2044
2351
  ignore_case: { type: 'boolean', description: 'Case-insensitive match.' },
2045
2352
  max_results: { type: 'integer', minimum: 1, maximum: 1000, description: 'Cap on matches (default 200).' },
2046
2353
  sessionId: SESSION_ID_INPUT,
@@ -2067,9 +2374,14 @@ patternMatcher) {
2067
2374
  },
2068
2375
  write: false,
2069
2376
  handler: async (a, ctx) => {
2070
- // The caller's pattern never runs on the request thread: a backtracking
2071
- // one would hold the event loop for everyone (see IPatternMatcher).
2072
- const matcher = patterns.search(a.pattern, a.ignore_case ? 'i' : '');
2377
+ // The caller's pattern runs in ripgrep, never on the request thread
2378
+ // (see IPatternMatcher); one the engine refuses is answered here,
2379
+ // before anything is searched. A caller that goes away takes the
2380
+ // search's runs with it.
2381
+ const search = await patterns.search(a.pattern, {
2382
+ ignoreCase: Boolean(a.ignore_case),
2383
+ signal: ctx.abortSignal,
2384
+ });
2073
2385
  // No path means the whole REPOSITORY, not the workspace directory above
2074
2386
  // it. The workspace root also holds whatever an older build left beside
2075
2387
  // the checkout, and the read gate has no rules for a path outside the
@@ -2095,11 +2407,17 @@ patternMatcher) {
2095
2407
  // worth a stat.
2096
2408
  // A placeholder named on its own is searched as what it is to every
2097
2409
  // other tool: nothing.
2098
- const kind = searchRoot === ''
2099
- ? 'directory'
2100
- : isFolderPlaceholder(searchRoot)
2101
- ? 'missing'
2102
- : await searchRootKind(fs, searchRoot);
2410
+ // A tmp ref names one file by rule (see `workspace-tmp.ts`), so it is
2411
+ // searched as a single file — never walked, and outside the repository
2412
+ // there is no read gate or read hook for it, as for `read_file`.
2413
+ const tmp = isTmpRef(searchRoot);
2414
+ const kind = tmp
2415
+ ? 'file'
2416
+ : searchRoot === ''
2417
+ ? 'directory'
2418
+ : isFolderPlaceholder(searchRoot)
2419
+ ? 'missing'
2420
+ : await searchRootKind(fs, searchRoot);
2103
2421
  /** Why a single-file search found nothing, when "no matches" would be a lie. */
2104
2422
  let fileNote;
2105
2423
  // The guide is searched where it is read: a search of the repository
@@ -2120,22 +2438,26 @@ patternMatcher) {
2120
2438
  // The guide's own path is not told here, because `guideAt` tells the
2121
2439
  // hook of the organisation's file there exactly as `read_file` does
2122
2440
  // (after the gate), and the platform's guide alone is nobody's file to
2123
- // note. Each file the walk opens goes to the hook per file below, so a
2124
- // hook sees every path a grep reached rather than only its root.
2125
- if (!(coversGuide && kind !== 'directory')) {
2441
+ // note. Each file the search opens goes to the hook per file below,
2442
+ // before it is opened and whether or not a line of it is answered
2443
+ // (`grepTree`'s admission), so a hook sees every file a grep reads
2444
+ // rather than only its root, and may refuse one as it refuses a
2445
+ // read_file. A tmp ref is no repository content, so there is nothing
2446
+ // to tell.
2447
+ if (!tmp && !(coversGuide && kind !== 'directory')) {
2126
2448
  await notifyAgentRead(agentAccessGate, ctx, a.branch, searchRoot);
2127
2449
  }
2450
+ const notifyRead = (p) => notifyAgentRead(agentAccessGate, ctx, a.branch, p);
2128
2451
  if (coversGuide) {
2129
2452
  if (kind === 'directory') {
2130
- await grepWalk(fs, searchRoot, matcher, out, max, 0, gate, (p) => notifyAgentRead(agentAccessGate, ctx, a.branch, p), docs, (p) => p === guidePath);
2131
- }
2132
- const lines = (await guideAt(a.branch, ctx, guidePath)).split('\n');
2133
- for (const i of await matcher.matchingLines(lines, max - out.length)) {
2134
- out.push({ path: guidePath, line: i + 1, text: lines[i].slice(0, 300) });
2453
+ out.push(...(await grepTree(fs, searchRoot, search, max, gate, notifyRead, docs, (p) => p === guidePath)));
2135
2454
  }
2455
+ const guide = await guideAt(a.branch, ctx, guidePath);
2456
+ const [hits] = await search.matchingLines([guide]);
2457
+ pushMatches(out, guidePath, guide, hits);
2136
2458
  }
2137
2459
  else if (kind === 'directory') {
2138
- await grepWalk(fs, searchRoot, matcher, out, max, 0, gate, (p) => notifyAgentRead(agentAccessGate, ctx, a.branch, p), docs);
2460
+ out.push(...(await grepTree(fs, searchRoot, search, max, gate, notifyRead, docs)));
2139
2461
  }
2140
2462
  else {
2141
2463
  // Not a directory: the permission verdict comes BEFORE every other
@@ -2147,11 +2469,28 @@ patternMatcher) {
2147
2469
  await assertCanRead(gate, searchRoot);
2148
2470
  if (kind === 'missing')
2149
2471
  throw notFound(searchRoot, 'Nothing to search');
2150
- // A named FILE is searched directly: routing it through the walk would
2151
- // fail its readdir and answer an empty match list, which the caller
2152
- // cannot tell from "the pattern is not in this file".
2153
- const outcome = await orNotFound(searchRoot, () => grepOneFile(fs, searchRoot, matcher, out, max, docs), 'Nothing to search');
2154
- if (outcome === 'no-text') {
2472
+ // A named text FILE is searched as a tree search searches it: by
2473
+ // ripgrep, from its folder, so it answers the same lines either way
2474
+ // (the valid UTF-8 lines of a file holding some that are not, and
2475
+ // nothing from one ripgrep finds binary). Any other file, and any
2476
+ // path that is not a regular file, is read through its reader,
2477
+ // exactly as read_file reads it, its text searched by the same
2478
+ // engine: a tree search would find nothing to walk and answer an
2479
+ // empty match list, which the caller cannot tell from "the pattern
2480
+ // is not in this file". A tmp ref is not in the checkout ripgrep
2481
+ // searches, so it is always read through its reader, its bytes from
2482
+ // the tmp resolver.
2483
+ const bytesOf = tmp
2484
+ ? (ref) => ctx.workspaceService.readTmpFile(docs.origin.workspaceId, ctx.user.id, ref)
2485
+ : fileBytes(fs);
2486
+ const found = (!tmp && readers.readerFor(searchRoot) instanceof TextReader
2487
+ ? await grepTextFile(fs, searchRoot, search, max, out)
2488
+ : null) ?? (await orNotFound(searchRoot, () => searchableText(bytesOf, searchRoot, docs), 'Nothing to search'));
2489
+ if (found.kind === 'text') {
2490
+ const [hits] = await search.matchingLines([found.text]);
2491
+ pushMatches(out, searchRoot, found.text, hits);
2492
+ }
2493
+ else if (found.kind === 'no-text') {
2155
2494
  fileNote =
2156
2495
  `"${displayPath(searchRoot)}" has no searchable text — it is an image, binary content, or a document ` +
2157
2496
  'whose text could not be extracted. There were no matches because there was nothing to search, not ' +
@@ -2164,8 +2503,11 @@ patternMatcher) {
2164
2503
  `extracted and this call's extraction budget (${UNCACHED_DOCS_PER_GREP}) ran out. Re-run the ` +
2165
2504
  'same grep to extract and search the next batch.'
2166
2505
  : undefined);
2506
+ // One order whatever found the lines, then the cap: the first
2507
+ // `max_results` by path, then line.
2508
+ out.sort((x, y) => compareSearchPaths(x.path, y.path) || x.line - y.line);
2167
2509
  return {
2168
- matches: out,
2510
+ matches: out.slice(0, max),
2169
2511
  truncated: out.length >= max,
2170
2512
  ...(note !== undefined ? { note } : {}),
2171
2513
  };
@@ -2180,6 +2522,7 @@ patternMatcher) {
2180
2522
  gated: true,
2181
2523
  description: 'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
2182
2524
  '`created`, `replaced` or `updated`.' +
2525
+ ASK_BEFORE_DEFAULT_BRANCH +
2183
2526
  UPLOAD_ROUTE_NOTE,
2184
2527
  inputs: {
2185
2528
  type: 'object',
@@ -2260,6 +2603,7 @@ patternMatcher) {
2260
2603
  '`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
2261
2604
  'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
2262
2605
  'does not stop the others; read `files` to see what landed.' +
2606
+ ASK_BEFORE_DEFAULT_BRANCH +
2263
2607
  UPLOAD_ROUTE_NOTE,
2264
2608
  inputs: {
2265
2609
  type: 'object',
@@ -2297,7 +2641,7 @@ patternMatcher) {
2297
2641
  enum: ['created', 'replaced', 'updated', 'refused'],
2298
2642
  description: 'What happened at this path. `refused` means nothing was written there and the file is untouched.',
2299
2643
  },
2300
- error: str('Present when `outcome` is `refused`: the refusal code — `exists`, `missing` or `binary_not_writable`.'),
2644
+ 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.'),
2301
2645
  message: str('Present when `outcome` is `refused`: the full refusal, the same one write_file would have given.'),
2302
2646
  },
2303
2647
  required: ['path', 'outcome'],
@@ -2426,6 +2770,7 @@ patternMatcher) {
2426
2770
  name: 'edit_file',
2427
2771
  gated: true,
2428
2772
  description: 'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
2773
+ ASK_BEFORE_DEFAULT_BRANCH +
2429
2774
  UPLOAD_ROUTE_NOTE,
2430
2775
  inputs: {
2431
2776
  type: 'object',
@@ -2504,7 +2849,8 @@ patternMatcher) {
2504
2849
  mount({
2505
2850
  name: 'delete_file',
2506
2851
  gated: true,
2507
- description: '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`.',
2852
+ description: '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`.' +
2853
+ ASK_BEFORE_DEFAULT_BRANCH,
2508
2854
  inputs: {
2509
2855
  type: 'object',
2510
2856
  properties: {
@@ -2726,10 +3072,11 @@ patternMatcher) {
2726
3072
  // the shared rules now, which are rebuilt from the layout where they live.
2727
3073
  description: '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. ' +
2728
3074
  'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
2729
- 'DESTINATION folder, so a move can change what you (and others) may do with the file: the dry run answers ' +
2730
- '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }`, where `access` is your ' +
2731
- 'own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the move has landed, ' +
2732
- 'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`. ' +
3075
+ '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, ' +
3076
+ 'or it is refused as read_file refuses. The dry run answers ' +
3077
+ '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, holderChanges?, allowed, reason? }`: your own ' +
3078
+ 'access at both ends, and who else loses or gains it. A move that changes either wants `confirm: true`; tell the person ' +
3079
+ 'who loses and who gains before you confirm. ' +
2733
3080
  '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.',
2734
3081
  inputs: {
2735
3082
  type: 'object',
@@ -2738,7 +3085,7 @@ patternMatcher) {
2738
3085
  src: wsPath(kbDirName, 'Source path (file or folder)'),
2739
3086
  dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
2740
3087
  dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
2741
- confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
3088
+ confirm: { type: 'boolean', description: 'Required when the move changes your access or anyone else\'s. Set it only after a dry run.' },
2742
3089
  rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
2743
3090
  sessionId: SESSION_ID_INPUT,
2744
3091
  },
@@ -2752,8 +3099,13 @@ patternMatcher) {
2752
3099
  dest: str('Destination path (echoes the input).'),
2753
3100
  kind: str('`file` or `folder`.'),
2754
3101
  descendants: int('Files that move: 1 for a file, the file count under a folder.'),
2755
- access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`).' },
3102
+ 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.' },
2756
3103
  accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after`.' },
3104
+ holderChanges: {
3105
+ type: 'object',
3106
+ description: 'Who loses and who gains opening (`read`) or editing (`write`) it once the move has landed: `{ lose, gain }`, each ' +
3107
+ '`[{ kind, name, email?, verb }]` with `kind` `group`, `role`, `plugin` or `person`. Absent when nobody\'s access changes.',
3108
+ },
2757
3109
  allowed: { type: 'boolean', description: 'Whether the move may run.' },
2758
3110
  reason: str('Why it may not, when `allowed` is false.'),
2759
3111
  dryRun: { type: 'boolean', description: 'True on a dry run.' },
@@ -2782,6 +3134,23 @@ patternMatcher) {
2782
3134
  // source already deleted.
2783
3135
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, src);
2784
3136
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, dest);
3137
+ // What moves and the folder it lands in must both be readable to the
3138
+ // caller, refused as read_file refuses them, on every branch: a move
3139
+ // preview reads both ends (the caller's own verdicts there, who else is
3140
+ // affected), and the folder's rules are what the names are made of. The
3141
+ // verdict comes BEFORE every other look, so neither end's existence is
3142
+ // told to someone who may not know it. The deployment's read hook is
3143
+ // asked first, as read_file asks it, and its refusal is the answer: a
3144
+ // move the hook would not let read is not previewed at all, since even
3145
+ // "this move asks for confirm" would tell what the comparison found. A
3146
+ // folder the caller may read moves with everything in it, files they
3147
+ // may not read included: those travel, they are not read.
3148
+ const destFolder = dest.slice(0, dest.lastIndexOf('/'));
3149
+ await notifyAgentRead(agentAccessGate, ctx, branch, src);
3150
+ await notifyAgentRead(agentAccessGate, ctx, branch, destFolder);
3151
+ const gate = readGateFor(branch, ctx);
3152
+ await assertCanRead(gate, src);
3153
+ await assertCanRead(gate, destFolder);
2785
3154
  const fs = await ctx.getFilesystem(branch);
2786
3155
  const root = await workspaceRoot(branch, ctx);
2787
3156
  // Before the kind check, which stats THROUGH a link: a dangling link at
@@ -2805,6 +3174,17 @@ patternMatcher) {
2805
3174
  accessAfter(branch, ctx, src, dest),
2806
3175
  ]);
2807
3176
  const accessChanges = verbsDiffer(before, after);
3177
+ // Who ELSE gains or loses opening or editing it: the lists the app's move
3178
+ // confirmation shows people. The caller may read both ends (the hook and
3179
+ // the rules, gated above, as the app's route gates), so the names go to
3180
+ // them, and a change asks for `confirm`. Outside the repository no rules
3181
+ // travel.
3182
+ const fromRel = toKbRelative(src, kbDirName);
3183
+ const toRel = toKbRelative(dest, kbDirName);
3184
+ const changes = fromRel !== null && toRel !== null
3185
+ ? await accessControl.prospectiveHolderChanges(workspaceIdForBranch(branch), fromRel, toRel)
3186
+ : undefined;
3187
+ const othersChange = changes !== undefined && (changes.lose.length > 0 || changes.gain.length > 0);
2808
3188
  // The placeholder moves with its folder, but it is never content.
2809
3189
  const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
2810
3190
  // Neither end may be the platform's own: a move neither takes a platform
@@ -2908,6 +3288,7 @@ patternMatcher) {
2908
3288
  descendants,
2909
3289
  access: { before, after },
2910
3290
  accessChanges,
3291
+ ...(othersChange ? { holderChanges: changes } : {}),
2911
3292
  allowed: moveReason === undefined,
2912
3293
  ...(moveReason !== undefined ? { reason: moveReason } : {}),
2913
3294
  ...(linkPlan ? { links: linkPlan.report } : {}),
@@ -2927,12 +3308,16 @@ patternMatcher) {
2927
3308
  throw new ToolError(reason, 409);
2928
3309
  if (linkPlan?.overCap !== undefined)
2929
3310
  throw new ToolError(linkPlan.overCap, 400);
2930
- if (accessChanges && a.confirm !== true) {
3311
+ if ((accessChanges || othersChange) && a.confirm !== true) {
3312
+ const why = [
3313
+ ...(accessChanges ? [`your access at "${dest}" differs from "${src}"`] : []),
3314
+ ...(othersChange ? ['it changes who else may open or edit it (`holderChanges`)'] : []),
3315
+ ];
2931
3316
  return {
2932
3317
  ...impact,
2933
3318
  confirmationRequired: true,
2934
3319
  moved: false,
2935
- message: `Nothing was moved: your access at "${dest}" differs from "${src}", so this move requires confirm: true.`,
3320
+ message: `Nothing was moved: ${why.join(', and ')}, so this move requires confirm: true.`,
2936
3321
  };
2937
3322
  }
2938
3323
  // The look above is what produces the sentence; the filesystem's own
@@ -2984,14 +3369,14 @@ patternMatcher) {
2984
3369
  mount({
2985
3370
  name: 'copy_file',
2986
3371
  gated: true,
2987
- description: '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. ' +
2988
- '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. ' +
2989
- '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.',
3372
+ description: '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. ' +
3373
+ '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. ' +
3374
+ '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.',
2990
3375
  inputs: {
2991
3376
  type: 'object',
2992
3377
  properties: {
2993
3378
  branch: BRANCH_INPUT,
2994
- src: wsPath(kbDirName, 'Source path'),
3379
+ src: orTmpRef(wsPath(kbDirName, 'Source path')),
2995
3380
  dest: wsPath(kbDirName, 'Destination path — must not exist yet'),
2996
3381
  dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
2997
3382
  sessionId: SESSION_ID_INPUT,
@@ -3019,11 +3404,16 @@ patternMatcher) {
3019
3404
  proposable: true,
3020
3405
  handler: async (a, ctx) => {
3021
3406
  // A copy CARRIES the source content into the destination, so BOTH ends
3022
- // go to the write hook. Ask about both before touching disk.
3023
- writePolicy.assertPathWritable(ctx.sessionId, a.src);
3407
+ // go to the write hook. Ask about both before touching disk. A tmp ref
3408
+ // is not a knowledge-base path, so only the destination is one to ask
3409
+ // about when the copy is out of the workspace's tmp folder.
3410
+ const fromTmp = isTmpRef(a.src);
3411
+ if (!fromTmp)
3412
+ writePolicy.assertPathWritable(ctx.sessionId, a.src);
3024
3413
  writePolicy.assertPathWritable(ctx.sessionId, a.dest);
3025
3414
  const branch = a.branch;
3026
- await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.src);
3415
+ if (!fromTmp)
3416
+ await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.src);
3027
3417
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.dest);
3028
3418
  const src = a.src;
3029
3419
  const dest = a.dest;
@@ -3073,6 +3463,10 @@ patternMatcher) {
3073
3463
  // the server's own absolute path. Anything that is not absence travels
3074
3464
  // on as it always did.
3075
3465
  const fs = await ctx.getFilesystem(branch);
3466
+ if (fromTmp) {
3467
+ await copyFromTmp(fs, branch, ctx, src, dest);
3468
+ return { src, dest, copied: true };
3469
+ }
3076
3470
  try {
3077
3471
  await asEntryExists(() => fs.copyFile(src, dest));
3078
3472
  }
@@ -3397,7 +3791,7 @@ patternMatcher) {
3397
3791
  '`replaced` / `updated`, or `refused` with `error` (the code) and `message` (why). `count` is how many landed and ' +
3398
3792
  '`total` how many paths there were; `files` is cut to the first 25 unless you pass `all: true`. ' +
3399
3793
  'Every path is judged one by one — by your write access, the platform-file rules and what is already there — ' +
3400
- 'exactly as `write_file` judges it, and a refused path does not stop the others. ' +
3794
+ 'as `write_file` judges it, except that platform files are refused and binary content is fine; a refused path does not stop the others. ' +
3401
3795
  'The token is single-use: it is spent by the apply that lands it, and refused if you use it twice, let it ' +
3402
3796
  'expire, or present one issued to somebody else. `mode` means what it means on `write_file`.',
3403
3797
  inputs: {
@@ -3476,7 +3870,7 @@ patternMatcher) {
3476
3870
  'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
3477
3871
  '`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
3478
3872
  'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
3479
- `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put every file back). ` +
3873
+ `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put them back). ` +
3480
3874
  'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
3481
3875
  '`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
3482
3876
  'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +