@bevel-software/platform-core-backend 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. package/agent-guide/access-control.md +19 -8
  2. package/agent-guide/html-views.md +231 -5
  3. package/agent-guide/tool-manuals.md +1 -1
  4. package/dist/core/catalog-revision.d.ts +12 -1
  5. package/dist/core/catalog-revision.d.ts.map +1 -1
  6. package/dist/core/catalog-revision.js +23 -3
  7. package/dist/core/catalog-revision.js.map +1 -1
  8. package/dist/core/core-ports.d.ts +41 -0
  9. package/dist/core/core-ports.d.ts.map +1 -1
  10. package/dist/core/core-ports.js.map +1 -1
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +14 -7
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +16 -5
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +37 -12
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +16 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -1
  20. package/dist/core/lifecycle.js +12 -2
  21. package/dist/core/lifecycle.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/modules/access/access-control.interface.d.ts +17 -9
  26. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  27. package/dist/modules/access/access-control.service.d.ts +12 -2
  28. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.js +164 -75
  30. package/dist/modules/access/access-control.service.js.map +1 -1
  31. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  32. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  33. package/dist/modules/access/access-mutation.service.js +48 -51
  34. package/dist/modules/access/access-mutation.service.js.map +1 -1
  35. package/dist/modules/access/access-view.d.ts.map +1 -1
  36. package/dist/modules/access/access-view.js +12 -11
  37. package/dist/modules/access/access-view.js.map +1 -1
  38. package/dist/modules/access/access.routes.d.ts +3 -0
  39. package/dist/modules/access/access.routes.d.ts.map +1 -1
  40. package/dist/modules/access/access.routes.js +35 -29
  41. package/dist/modules/access/access.routes.js.map +1 -1
  42. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  43. package/dist/modules/access/groups-admin.routes.js +4 -2
  44. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  45. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  46. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  47. package/dist/modules/access/groups-admin.service.js +80 -38
  48. package/dist/modules/access/groups-admin.service.js.map +1 -1
  49. package/dist/modules/access/groups-edit.d.ts +1 -1
  50. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  51. package/dist/modules/access/groups-edit.js +1 -1
  52. package/dist/modules/access/groups-edit.js.map +1 -1
  53. package/dist/modules/access/reference-scan.d.ts +116 -24
  54. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  55. package/dist/modules/access/reference-scan.js +305 -44
  56. package/dist/modules/access/reference-scan.js.map +1 -1
  57. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  58. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  59. package/dist/modules/access/roles-admin.service.js +32 -23
  60. package/dist/modules/access/roles-admin.service.js.map +1 -1
  61. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  62. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  63. package/dist/modules/access-model/access-grammar.js +60 -20
  64. package/dist/modules/access-model/access-grammar.js.map +1 -1
  65. package/dist/modules/access-model/access-splice.d.ts +22 -18
  66. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  67. package/dist/modules/access-model/access-splice.js +20 -14
  68. package/dist/modules/access-model/access-splice.js.map +1 -1
  69. package/dist/modules/access-model/group-files.d.ts +1 -0
  70. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  71. package/dist/modules/access-model/group-files.js +5 -1
  72. package/dist/modules/access-model/group-files.js.map +1 -1
  73. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  74. package/dist/modules/agent-guide/agent-guide.tools.js +13 -3
  75. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  76. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  77. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  78. package/dist/modules/agent-instructions/compose.js +3 -1
  79. package/dist/modules/agent-instructions/compose.js.map +1 -1
  80. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  81. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  82. package/dist/modules/agent-instructions/shared-file-rules.js +16 -1
  83. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  84. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  85. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  86. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  87. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  88. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  89. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  90. package/dist/modules/agent-scope/connection-scope.js +165 -0
  91. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  92. package/dist/modules/agent-scope/index.d.ts +3 -0
  93. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  94. package/dist/modules/agent-scope/index.js +3 -0
  95. package/dist/modules/agent-scope/index.js.map +1 -0
  96. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  97. package/dist/modules/audit/agent-audit.service.js +2 -0
  98. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  99. package/dist/modules/audit/audit.contract.d.ts +6 -0
  100. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  101. package/dist/modules/audit/audit.contract.js.map +1 -1
  102. package/dist/modules/auth/account-admission.d.ts +12 -0
  103. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  104. package/dist/modules/auth/account-admission.js +15 -0
  105. package/dist/modules/auth/account-admission.js.map +1 -1
  106. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  107. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  108. package/dist/modules/auth/auth.middleware.js +42 -4
  109. package/dist/modules/auth/auth.middleware.js.map +1 -1
  110. package/dist/modules/auth/auth.routes.d.ts +13 -9
  111. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  112. package/dist/modules/auth/auth.routes.js +35 -29
  113. package/dist/modules/auth/auth.routes.js.map +1 -1
  114. package/dist/modules/auth/auth.service.d.ts +55 -6
  115. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  116. package/dist/modules/auth/auth.service.js +128 -34
  117. package/dist/modules/auth/auth.service.js.map +1 -1
  118. package/dist/modules/auth/index.d.ts +1 -1
  119. package/dist/modules/auth/index.d.ts.map +1 -1
  120. package/dist/modules/auth/index.js +1 -1
  121. package/dist/modules/auth/index.js.map +1 -1
  122. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  123. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  124. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  125. package/dist/modules/database/core-schema.d.ts +68 -0
  126. package/dist/modules/database/core-schema.d.ts.map +1 -1
  127. package/dist/modules/database/core-schema.js +33 -0
  128. package/dist/modules/database/core-schema.js.map +1 -1
  129. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  130. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  131. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  132. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  133. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  134. package/dist/modules/embed/embed.tools.js +5 -0
  135. package/dist/modules/embed/embed.tools.js.map +1 -1
  136. package/dist/modules/kb-fs/index.d.ts +2 -0
  137. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  138. package/dist/modules/kb-fs/index.js +1 -0
  139. package/dist/modules/kb-fs/index.js.map +1 -1
  140. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  141. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  142. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  143. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  144. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  145. package/dist/modules/kb-fs/repo-path.js +22 -7
  146. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  147. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  148. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  149. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  150. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  153. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  154. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  155. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  156. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  157. package/dist/modules/mcp/mcp.routes.js +39 -4
  158. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  159. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  160. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  161. package/dist/modules/mcp/mcp.service.js +68 -13
  162. package/dist/modules/mcp/mcp.service.js.map +1 -1
  163. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  164. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  165. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  167. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  168. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  169. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  171. package/dist/modules/plugins/index.d.ts +1 -1
  172. package/dist/modules/plugins/index.d.ts.map +1 -1
  173. package/dist/modules/plugins/index.js.map +1 -1
  174. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  175. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-links.js.map +1 -1
  177. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  178. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  179. package/dist/modules/plugins/plugins.routes.js +32 -5
  180. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  181. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  182. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  183. package/dist/modules/secrets-vault/secrets-vault.routes.js +15 -3
  184. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  185. package/dist/modules/skills/index.d.ts +3 -3
  186. package/dist/modules/skills/index.d.ts.map +1 -1
  187. package/dist/modules/skills/index.js +0 -2
  188. package/dist/modules/skills/index.js.map +1 -1
  189. package/dist/modules/skills/skills.contract.d.ts +16 -0
  190. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  192. package/dist/modules/skills/skills.service.js +6 -1
  193. package/dist/modules/skills/skills.service.js.map +1 -1
  194. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  195. package/dist/modules/skills/skills.tools.js +24 -12
  196. package/dist/modules/skills/skills.tools.js.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  198. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  201. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  202. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  203. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  204. package/dist/modules/tool-auth/index.d.ts +1 -1
  205. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  206. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  207. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  208. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  209. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  210. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  211. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  212. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  213. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  214. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  215. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  216. package/dist/modules/tool-helpers/tool-context.js +3 -0
  217. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  218. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  219. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  220. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  221. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  222. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  223. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  224. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  225. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  226. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  227. package/dist/modules/tool-helpers/validate-token.js +1 -1
  228. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  229. package/dist/modules/tool-manuals/index.d.ts +3 -3
  230. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  231. package/dist/modules/tool-manuals/index.js +0 -2
  232. package/dist/modules/tool-manuals/index.js.map +1 -1
  233. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +19 -6
  234. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  235. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  236. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  238. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  239. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +13 -6
  240. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  241. package/dist/modules/tool-manuals/tool-manuals.service.js +23 -15
  242. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  243. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.tools.js +25 -10
  245. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  246. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  247. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  248. package/dist/modules/tool-registry/manual.routes.js +27 -7
  249. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  250. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  251. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  252. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  253. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  254. package/dist/modules/workflow/agent-tools/workflow.tools.js +14 -9
  255. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  256. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  257. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  258. package/dist/modules/workflow/git/git.service.js +20 -35
  259. package/dist/modules/workflow/git/git.service.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +3 -0
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  265. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  266. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  267. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  268. package/dist/modules/workspace/index.d.ts +8 -0
  269. package/dist/modules/workspace/index.d.ts.map +1 -1
  270. package/dist/modules/workspace/index.js +8 -0
  271. package/dist/modules/workspace/index.js.map +1 -1
  272. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  273. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  274. package/dist/modules/workspace/nul-scan.js +55 -0
  275. package/dist/modules/workspace/nul-scan.js.map +1 -0
  276. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  277. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  278. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  279. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  280. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  281. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  282. package/dist/modules/workspace/ripgrep-search.js +657 -0
  283. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  284. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  285. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  286. package/dist/modules/workspace/ripgrep-version.js +31 -0
  287. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  288. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  289. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  290. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  291. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  292. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  293. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  294. package/dist/modules/workspace/worker-pool.js.map +1 -1
  295. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  296. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  297. package/dist/modules/workspace/workspace-tmp.js +435 -0
  298. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  299. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  300. package/dist/modules/workspace/workspace.routes.js +5 -26
  301. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  302. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  303. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  304. package/dist/modules/workspace/workspace.service.js +86 -6
  305. package/dist/modules/workspace/workspace.service.js.map +1 -1
  306. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  307. package/dist/modules/workspace/workspace.tools.js +460 -116
  308. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  309. package/dist/shared/path-order.d.ts +28 -8
  310. package/dist/shared/path-order.d.ts.map +1 -1
  311. package/dist/shared/path-order.js +37 -8
  312. package/dist/shared/path-order.js.map +1 -1
  313. package/migrations/0022_session_version.sql +6 -0
  314. package/migrations/0023_agent_path.sql +3 -0
  315. package/migrations/meta/0022_snapshot.json +2360 -0
  316. package/migrations/meta/0023_snapshot.json +2378 -0
  317. package/migrations/meta/_journal.json +14 -0
  318. package/package.json +3 -3
  319. package/src/__tests__/pg-scratch.test.ts +69 -0
  320. package/src/__tests__/pg-scratch.ts +31 -0
  321. package/src/__tests__/slow-ripgrep.ts +65 -0
  322. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  323. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  324. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  325. package/src/core/__tests__/gated-boot.test.ts +8 -1
  326. package/src/core/__tests__/lifecycle.test.ts +46 -13
  327. package/src/core/catalog-revision.ts +24 -2
  328. package/src/core/core-ports.ts +42 -0
  329. package/src/core/create-core-server.ts +13 -0
  330. package/src/core/create-core-services.ts +49 -12
  331. package/src/core/lifecycle.ts +26 -2
  332. package/src/index.ts +1 -1
  333. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  334. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  335. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  336. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  337. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  338. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  339. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  340. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  341. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  342. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  343. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  344. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  345. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  346. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  347. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  348. package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -0
  349. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  350. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  351. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  352. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  353. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  354. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  355. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  356. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  357. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  358. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  359. package/src/modules/access/access-control.interface.ts +17 -10
  360. package/src/modules/access/access-control.service.ts +158 -72
  361. package/src/modules/access/access-mutation.service.ts +48 -53
  362. package/src/modules/access/access-view.ts +12 -10
  363. package/src/modules/access/access.routes.ts +43 -33
  364. package/src/modules/access/groups-admin.routes.ts +4 -2
  365. package/src/modules/access/groups-admin.service.ts +114 -44
  366. package/src/modules/access/groups-edit.ts +1 -1
  367. package/src/modules/access/reference-scan.ts +319 -46
  368. package/src/modules/access/roles-admin.service.ts +43 -26
  369. package/src/modules/access/synced-groups-writer.ts +1 -1
  370. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  371. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  372. package/src/modules/access-model/access-grammar.ts +65 -20
  373. package/src/modules/access-model/access-splice.ts +31 -23
  374. package/src/modules/access-model/group-files.ts +5 -0
  375. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +31 -0
  376. package/src/modules/agent-guide/agent-guide.tools.ts +13 -2
  377. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +1 -0
  378. package/src/modules/agent-instructions/compose.ts +3 -1
  379. package/src/modules/agent-instructions/shared-file-rules.ts +17 -1
  380. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  381. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  382. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  383. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  384. package/src/modules/agent-scope/connection-scope.ts +199 -0
  385. package/src/modules/agent-scope/index.ts +15 -0
  386. package/src/modules/audit/agent-audit.service.ts +2 -0
  387. package/src/modules/audit/audit.contract.ts +6 -0
  388. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  389. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  390. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  391. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  392. package/src/modules/auth/account-admission.ts +19 -0
  393. package/src/modules/auth/auth.middleware.ts +45 -4
  394. package/src/modules/auth/auth.routes.ts +46 -29
  395. package/src/modules/auth/auth.service.ts +135 -34
  396. package/src/modules/auth/index.ts +2 -0
  397. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  398. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  399. package/src/modules/database/core-schema.ts +33 -0
  400. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  401. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  402. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  403. package/src/modules/embed/embed.tools.ts +5 -0
  404. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  405. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  406. package/src/modules/kb-fs/index.ts +2 -0
  407. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  408. package/src/modules/kb-fs/repo-path.ts +33 -7
  409. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  410. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  411. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  412. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  413. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  414. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  415. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  416. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  417. package/src/modules/mcp/mcp.routes.ts +39 -3
  418. package/src/modules/mcp/mcp.service.ts +107 -17
  419. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  420. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  421. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  422. package/src/modules/plugins/index.ts +1 -1
  423. package/src/modules/plugins/plugin-links.ts +19 -2
  424. package/src/modules/plugins/plugins.routes.ts +35 -4
  425. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  426. package/src/modules/secrets-vault/secrets-vault.routes.ts +23 -3
  427. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  428. package/src/modules/skills/index.ts +7 -2
  429. package/src/modules/skills/skills.contract.ts +17 -0
  430. package/src/modules/skills/skills.service.ts +5 -1
  431. package/src/modules/skills/skills.tools.ts +44 -13
  432. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  433. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  434. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  435. package/src/modules/tool-auth/index.ts +1 -1
  436. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  437. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  438. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  439. package/src/modules/tool-helpers/tool-context.ts +10 -0
  440. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  441. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  442. package/src/modules/tool-helpers/validate-token.ts +4 -2
  443. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  444. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  445. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  446. package/src/modules/tool-manuals/index.ts +6 -2
  447. package/src/modules/tool-manuals/tool-manuals.contract.ts +18 -6
  448. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  449. package/src/modules/tool-manuals/tool-manuals.service.ts +31 -15
  450. package/src/modules/tool-manuals/tool-manuals.tools.ts +28 -11
  451. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +2 -2
  452. package/src/modules/tool-registry/manual.routes.ts +26 -5
  453. package/src/modules/tool-registry/tool.contract.ts +7 -0
  454. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  455. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  457. package/src/modules/workflow/agent-tools/workflow.tools.ts +14 -9
  458. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  459. package/src/modules/workflow/git/git.service.ts +29 -48
  460. package/src/modules/workflow/workflow.service.ts +5 -0
  461. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  462. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  463. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  464. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  465. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  466. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  467. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  468. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  469. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -2
  470. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  471. package/src/modules/workspace/__tests__/workspace.tools.test.ts +393 -113
  472. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  473. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  474. package/src/modules/workspace/index.ts +20 -0
  475. package/src/modules/workspace/nul-scan.ts +50 -0
  476. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  477. package/src/modules/workspace/ripgrep-search.ts +715 -0
  478. package/src/modules/workspace/ripgrep-version.ts +35 -0
  479. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  480. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  481. package/src/modules/workspace/worker-pool.ts +3 -4
  482. package/src/modules/workspace/workspace-tmp.ts +457 -0
  483. package/src/modules/workspace/workspace.routes.ts +6 -27
  484. package/src/modules/workspace/workspace.service.ts +92 -15
  485. package/src/modules/workspace/workspace.tools.ts +500 -155
  486. package/src/shared/path-order.ts +37 -8
  487. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  488. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  489. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  490. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  491. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  492. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  493. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  494. package/dist/modules/workspace/bounded-matcher.js +0 -200
  495. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  496. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  497. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -5,6 +5,7 @@ const log = logger('declared-variables');
5
5
  import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
6
6
  import type { IToolManualService } from '../tool-manuals/index.js';
7
7
  import type { ISecretsVaultService } from '../secrets-vault/index.js';
8
+ import { resolveConnectionScope, type IAgentScopeResolver } from '../agent-scope/index.js';
8
9
 
9
10
  type ResolveUserEmail = (userId: string) => Promise<string | undefined>;
10
11
 
@@ -48,6 +49,12 @@ export function createDeclaredVariableRoutes(
48
49
  secretsVault: ISecretsVaultService,
49
50
  manualAuth: RequestHandler,
50
51
  resolveUserEmail: ResolveUserEmail,
52
+ /**
53
+ * Resolves the scope of a connection made as an agent configuration: such a
54
+ * connection is handed only the variables of a connector the file loads.
55
+ * Absent, a narrowed connection is handed none.
56
+ */
57
+ agentScope?: Pick<IAgentScopeResolver, 'resolve'>,
51
58
  ): express.Router {
52
59
  const router = express.Router();
53
60
 
@@ -125,7 +132,11 @@ export function createDeclaredVariableRoutes(
125
132
  // `listAccessible` already applies the per-file read verdict, so an
126
133
  // unreadable manual is simply absent — same fail-closed read model the
127
134
  // catalog uses.
128
- const manual = (await toolManualService.listAccessible(who.email)).find((m) => m.slug === slug);
135
+ const found = (await toolManualService.listAccessible(who.email)).find((m) => m.slug === slug);
136
+ // A connection made as an agent configuration is handed only the variables of
137
+ // a connector it loads; another is as absent as an unreadable one.
138
+ const scope = found ? await resolveConnectionScope(agentScope, who.email, req.toolAuth?.agentPath) : null;
139
+ const manual = found && scope?.allowsConnector(found.path) ? found : undefined;
129
140
  if (!manual) return void res.status(404).json({ error: 'Not found' });
130
141
  if (manual.remote !== false) {
131
142
  return void res.status(409).json({
@@ -281,6 +281,18 @@ describe('open_page: refusals', () => {
281
281
  expect(embedService.mintForUser).not.toHaveBeenCalled();
282
282
  });
283
283
 
284
+ it('refuses a tmp ref — no knowledge-base page — reading and minting nothing', async () => {
285
+ const read = vi.fn<ReadForTool>();
286
+ const { call, embedService } = await serve({ read });
287
+ const { status, body } = await call({ path: '__tmp__/chat-uploads/plan.xlsx' });
288
+ expect(status).toBe(400);
289
+ expect(body.kind).toBe('tmp-ref-read-only');
290
+ expect(body.error).toContain('open_page does not take one there');
291
+ expect(body.error).toContain('copy_file');
292
+ expect(read).not.toHaveBeenCalled();
293
+ expect(embedService.mintForUser).not.toHaveBeenCalled();
294
+ });
295
+
284
296
  it('refuses a path outside the knowledge base, and a missing one', async () => {
285
297
  const { call, embedService } = await serve();
286
298
  expect((await call({ path: '' })).status).toBe(400);
@@ -6,6 +6,7 @@ import type { KbContext } from '../../shared/kb-context.js';
6
6
  import type { ReadForTool } from '../workspace/index.js';
7
7
  import type { IEmbedService } from './embed.interface.js';
8
8
  import { requireExternalSource } from '../tool-auth/index.js';
9
+ import { assertTmpRefsTaken } from '../kb-fs/index.js';
9
10
  import { SESSION_ID_INPUT, type ToolDescriptionNotes, sessionIdInputOf } from '../workspace/index.js';
10
11
 
11
12
  /** The tool name, in one place: the app manifest keys its view by it. */
@@ -149,6 +150,10 @@ export function registerEmbedTools(
149
150
  toolHandler(async (args, ctx: ToolContext) => {
150
151
  const raw = typeof args.path === 'string' ? args.path.trim() : '';
151
152
  if (!raw) throw new ToolError('`path` is required.', 400);
153
+ // A tmp ref (`__tmp__/…`) is no knowledge-base page: refused by the one
154
+ // admission gate the normalised tools pass, before `toRepoRelative`
155
+ // would read it as the repository path `<kbDirName>/__tmp__/…`.
156
+ assertTmpRefsTaken(OPEN_PAGE_TOOL, { path: raw });
152
157
  const heading = typeof args.heading === 'string' && args.heading.trim() ? args.heading.trim() : undefined;
153
158
  const repoRelative = toRepoRelative(raw, kbDirName);
154
159
  if (repoRelative === null) throw new ToolError(OUTSIDE_REPO(kbDirName), 400);
@@ -1482,6 +1482,31 @@ describe('LockingFilesystem — a destination that is taken is refused, never re
1482
1482
  expect(await fs.readFile(onDisk(TAKEN), 'utf-8')).toBe('the winner of the race');
1483
1483
  });
1484
1484
 
1485
+ /**
1486
+ * The same race for an EXCLUSIVE write (`overwrite: false`), which is how
1487
+ * `copy_file` lands bytes read out of the workspace's tmp folder: the
1488
+ * refusal is the clash sentence, and the lock releases untouched rather
1489
+ * than discarding the winner's queued file.
1490
+ */
1491
+ it('an exclusive write onto a taken name is the same refusal, and releases UNTOUCHED', async () => {
1492
+ const workflow = makeWorkflow();
1493
+ const layer = new LockingFilesystem(
1494
+ { basePath: root, contained: true },
1495
+ { workflow, workspaceId: 'ws-feat', branch: 'feat', user: USER, kbDirName: KB },
1496
+ );
1497
+ await fs.writeFile(onDisk(TAKEN), 'the winner of the race');
1498
+
1499
+ await expect(layer.writeFile(TAKEN, 'the loser', { overwrite: false })).rejects.toMatchObject({
1500
+ name: 'DestinationTakenError',
1501
+ message: 'A file named taken.md already exists in KnowledgeBase.',
1502
+ });
1503
+
1504
+ expect(workflow.releaseLockUntouched).toHaveBeenCalledWith('ws-feat', 'feat', TAKEN, USER);
1505
+ expect(workflow.releaseLockNoCommit).not.toHaveBeenCalled();
1506
+ expect(workflow.releaseLock).not.toHaveBeenCalled();
1507
+ expect(await fs.readFile(onDisk(TAKEN), 'utf-8')).toBe('the winner of the race');
1508
+ });
1509
+
1485
1510
  it.skipIf(process.platform === 'win32')('answers the clash sentence when a DANGLING link holds the name', async () => {
1486
1511
  // A link to nothing still owns the directory entry, so the exclusive
1487
1512
  // create fails — and an existence probe that follows links would say the
@@ -231,8 +231,8 @@ describe('a line break in a path — one name to the gates, two object names to
231
231
  });
232
232
 
233
233
  it('refuses it through normalizePathArgs, which is where the MCP tools meet the rule', () => {
234
- expect(() => normalizePathArgs({ path: INJECTED }, KB)).toThrow(/control character/);
235
- expect(() => normalizePathArgs({ files: [{ path: INJECTED }] }, KB)).toThrow(/control character/);
234
+ expect(() => normalizePathArgs({ path: INJECTED }, KB, { tool: 'read_file' })).toThrow(/control character/);
235
+ expect(() => normalizePathArgs({ files: [{ path: INJECTED }] }, KB, { tool: 'read_file' })).toThrow(/control character/);
236
236
  });
237
237
  });
238
238
 
@@ -360,6 +360,8 @@ describe('assertRepoRootNameFree', () => {
360
360
  });
361
361
 
362
362
  describe('normalizePathArgs', () => {
363
+ const T = { tool: 'read_file' };
364
+
363
365
  it('normalizes every path-shaped tool argument, batch entries included', () => {
364
366
  expect(
365
367
  normalizePathArgs(
@@ -372,6 +374,7 @@ describe('normalizePathArgs', () => {
372
374
  files: [{ path: 'e.md', content: '/x' }],
373
375
  },
374
376
  KB,
377
+ T,
375
378
  ),
376
379
  ).toEqual({
377
380
  branch: '/main',
@@ -385,22 +388,64 @@ describe('normalizePathArgs', () => {
385
388
 
386
389
  it('leaves a spill ref alone — it belongs to no workspace', () => {
387
390
  expect(
388
- normalizePathArgs({ path: '__tool_chain_spill__/abc.json' }, KB, (v) => v.startsWith('__tool_chain_spill__/')),
391
+ normalizePathArgs({ path: '__tool_chain_spill__/abc.json' }, KB, { tool: 'read_file', skip: (v) => v.startsWith('__tool_chain_spill__/') }),
389
392
  ).toEqual({ path: '__tool_chain_spill__/abc.json' });
390
393
  });
391
394
 
395
+ it('still takes the earlier call form, a skip predicate or nothing, and refuses a tmp ref in every input there', () => {
396
+ const spill = (v: string) => v.startsWith('__tool_chain_spill__/');
397
+ expect(normalizePathArgs({ path: '__tool_chain_spill__/abc.json' }, KB, spill)).toEqual({ path: '__tool_chain_spill__/abc.json' });
398
+ expect(normalizePathArgs({ path: 'a.md' }, KB)).toEqual({ path: 'knowledge-base/a.md' });
399
+ for (const legacy of [undefined, spill]) {
400
+ expect(() => normalizePathArgs({ path: '__tmp__/x.md' }, KB, legacy)).toThrow(/this tool does not take one there/);
401
+ }
402
+ });
403
+
392
404
  it('leaves an absent or empty path to the handler\'s own required-argument message', () => {
393
- expect(normalizePathArgs({ path: undefined }, KB)).toEqual({ path: undefined });
394
- expect(normalizePathArgs({ path: '' }, KB)).toEqual({ path: '' });
395
- expect(normalizePathArgs({ branch: 'main' }, KB)).toEqual({ branch: 'main' });
405
+ expect(normalizePathArgs({ path: undefined }, KB, T)).toEqual({ path: undefined });
406
+ expect(normalizePathArgs({ path: '' }, KB, T)).toEqual({ path: '' });
407
+ expect(normalizePathArgs({ branch: 'main' }, KB, T)).toEqual({ branch: 'main' });
396
408
  });
397
409
 
398
410
  it('collapses the spellings so two callers take one lock row', () => {
399
- expect(normalizePathArgs({ path: './knowledge-base//a.md' }, KB)).toEqual({ path: 'knowledge-base/a.md' });
411
+ expect(normalizePathArgs({ path: './knowledge-base//a.md' }, KB, T)).toEqual({ path: 'knowledge-base/a.md' });
412
+ });
413
+
414
+ it('admits a tmp ref, as written, in exactly the inputs the tool names', () => {
415
+ expect(normalizePathArgs({ src: '__tmp__/a.pdf', dest: 'a.pdf' }, KB, { tool: 'copy_file', tmpRefInputs: ['src'] })).toEqual({
416
+ src: '__tmp__/a.pdf',
417
+ dest: 'knowledge-base/a.pdf',
418
+ });
419
+ });
420
+
421
+ it('refuses a tmp ref in every other input, lists included, naming the tool and copy_file', () => {
422
+ const calls: Array<Record<string, unknown>> = [
423
+ { path: '__tmp__/a.md' },
424
+ { src: '__tmp__/a.md' },
425
+ { dest: '__tmp__/a.md' },
426
+ { destination: '__tmp__' },
427
+ { files: [{ path: 'ok.md' }, { path: '__tmp__/a.md' }] },
428
+ { paths: ['ok.md', '__tmp__/a.md'] },
429
+ { resolutions: [{ path: 'ok.md', choose: 'ours' }, { path: '__tmp__/a.md', content: 'x' }] },
430
+ ];
431
+ for (const args of calls) {
432
+ expect(() => normalizePathArgs(args, KB, { tool: 'save_file' })).toThrow(
433
+ /is a tmp ref, and save_file does not take one there: the workspace's tmp folder is read-only through the ref\..*copy_file/,
434
+ );
435
+ }
436
+ // A tool that takes a ref in `src` still refuses one in `dest`.
437
+ expect(() => normalizePathArgs({ dest: '__tmp__/a.md' }, KB, { tool: 'copy_file', tmpRefInputs: ['src'] })).toThrow(
438
+ /copy_file does not take one there/,
439
+ );
440
+ });
441
+
442
+ it('reads a path that only contains the prefix as an ordinary repository path', () => {
443
+ expect(normalizePathArgs({ path: 'knowledge-base/__tmp__/a.md' }, KB, T)).toEqual({ path: 'knowledge-base/__tmp__/a.md' });
444
+ expect(normalizePathArgs({ path: '/knowledge-base/__tmp__/a.md' }, KB, T)).toEqual({ path: 'knowledge-base/__tmp__/a.md' });
400
445
  });
401
446
 
402
447
  it('refuses a traversing tool argument', () => {
403
- expect(() => normalizePathArgs({ path: '../escape.md' }, KB)).toThrow(/outside the knowledge base repository/);
448
+ expect(() => normalizePathArgs({ path: '../escape.md' }, KB, T)).toThrow(/outside the knowledge base repository/);
404
449
  });
405
450
  });
406
451
 
@@ -8,6 +8,7 @@ export { WorkspaceMutex, WorkspaceIdMutex } from './mutex.js';
8
8
  export { readRefSha, readRevSha, type RefFileAnswer } from './ref-files.js';
9
9
  export { gitDirOf, readGitfile } from './git-dir.js';
10
10
  export { normalizeWorkspacePath } from './repo-path.js';
11
+ export { TMP_REF_PREFIX, assertTmpRefsTaken, isTmpRef, tmpRefReadOnlyRefusal } from './tmp-ref.js';
11
12
  // Reached by other modules; exported here so they stop importing the files.
12
13
  export { BevelIgnoreStack } from './bevel-ignore.js';
13
14
  export { assertValidBranchName, assertValidRelativePath, isBranchAuthoredBy, isOwnSuggestionsBranch } from './branch-name.js';
@@ -18,3 +19,4 @@ export { LockingFilesystem, MoveLockedError, MoveRacedError } from './locking-fi
18
19
  export { ReadOnlyFilesystem } from './read-only-filesystem.js';
19
20
  export { normalizeRepositoryAddress, sameRepository } from './remote-url.js';
20
21
  export { assertInsideRepo, assertKbDirNameFree, assertRepoRootNameFree, assertRepoRootNameFreeArgs, hasControlCharacter, isInsideRepo, normalizePathArgs } from './repo-path.js';
22
+ export type { NormalizePathArgsOptions } from './repo-path.js';
@@ -226,9 +226,15 @@ export class LockingFilesystem extends GitGuardedFilesystem {
226
226
  const plan = await this.planCreate(inputPath, 'file');
227
227
  if (plan?.kind === 'seed-access-md') await this.seedAccessMd(plan);
228
228
  const toWrite = content;
229
+ // An EXCLUSIVE write (`overwrite: false`, the `wx` flag) that finds the
230
+ // name taken is refused as a copy is (`asTakenRefusal`): the refusal is a
231
+ // `DestinationTakenError`, so the lock is released untouched. Left as the
232
+ // raw failure it would be a failed op, and the discard that follows resets
233
+ // the PATH — throwing away the winner's file, whose commit is still queued.
234
+ const land = () => super.writeFile(inputPath, toWrite, options);
229
235
  return this.withLock(
230
236
  inputPath,
231
- () => super.writeFile(inputPath, toWrite, options),
237
+ () => (options?.overwrite === false ? this.asTakenRefusal(inputPath, land) : land()),
232
238
  (check || validate) &&
233
239
  (async () => {
234
240
  // The caller's verdict first: it decides whether this write happens
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { WorkflowValidationError } from '../../shared/domain-errors.js';
3
3
  import { sanitizedPath } from '../../shared/printable.js';
4
+ import { assertTmpRefsTaken, isTmpRef } from './tmp-ref.js';
4
5
 
5
6
  /**
6
7
  * Workspace-relative paths and the repository folder.
@@ -312,6 +313,16 @@ export function assertKbDirNameFree(
312
313
  );
313
314
  }
314
315
 
316
+ /** How {@link normalizePathArgs} treats one tool's call. */
317
+ export interface NormalizePathArgsOptions {
318
+ /** The tool the call is to, named in a tmp-ref refusal. */
319
+ tool: string;
320
+ /** The inputs of `tool` that take a tmp ref (`__tmp__/…`); every other refuses one. */
321
+ tmpRefInputs?: readonly string[];
322
+ /** A value that is no workspace path at all, passed to the handler as written. */
323
+ skip?: (value: string) => boolean;
324
+ }
325
+
315
326
  /** The tool inputs that carry a workspace path. */
316
327
  const PATH_ARG_KEYS = ['path', 'src', 'dest', 'destination'] as const;
317
328
 
@@ -321,19 +332,34 @@ const PATH_ARG_KEYS = ['path', 'src', 'dest', 'destination'] as const;
321
332
  * copy. This is where the MCP surface meets the normaliser — one call, before
322
333
  * any handler runs, so no tool keeps a resolver of its own.
323
334
  *
324
- * `skip` is for the one input that is not a workspace path at all: `read_file`
325
- * also takes a `__tool_chain_spill__/…` ref, which belongs to no workspace and
326
- * must reach the handler as written. An absent or non-string value is left
327
- * alone too — the handler's own required-argument message is better than a
328
- * path refusal about `undefined`.
335
+ * It is also where a tmp ref (`__tmp__/…`) is admitted or refused, first:
336
+ * {@link assertTmpRefsTaken} refuses one in every input of `tool` that
337
+ * `tmpRefInputs` does not name, and a ref in an input that does name it
338
+ * reaches the handler as written — so no tool whose paths pass through here
339
+ * can read the ref as the repository path `<kbDirName>/__tmp__/…`.
340
+ *
341
+ * `skip` is for the other input that is not a workspace path at all:
342
+ * `read_file` also takes a `__tool_chain_spill__/…` ref, which belongs to no
343
+ * workspace and must reach the handler as written. An absent or non-string
344
+ * value is left alone too — the handler's own required-argument message is
345
+ * better than a path refusal about `undefined`.
346
+ *
347
+ * The earlier call form — a `skip` predicate (or nothing) as the third
348
+ * argument — is still taken, for a distribution's own tool written against
349
+ * it: such a call names no tool and takes a tmp ref in no input, so the ref is
350
+ * refused there as read-only, under the name "this tool".
329
351
  */
330
352
  export function normalizePathArgs(
331
353
  args: Record<string, unknown>,
332
354
  kbDirName: string,
333
- skip?: (value: string) => boolean,
355
+ options?: NormalizePathArgsOptions | ((value: string) => boolean),
334
356
  ): Record<string, unknown> {
357
+ const { tool, tmpRefInputs, skip } =
358
+ typeof options === 'object' ? options : { tool: 'this tool', tmpRefInputs: undefined, skip: options };
359
+ assertTmpRefsTaken(tool, args, tmpRefInputs);
360
+ // Every tmp ref still here is in an input that takes one.
335
361
  const one = (value: unknown): unknown =>
336
- typeof value !== 'string' || value.length === 0 || skip?.(value)
362
+ typeof value !== 'string' || value.length === 0 || isTmpRef(value) || skip?.(value)
337
363
  ? value
338
364
  : normalizeWorkspacePath(value, kbDirName);
339
365
  const out: Record<string, unknown> = { ...args };
@@ -0,0 +1,81 @@
1
+ import { WorkflowValidationError } from '../../shared/domain-errors.js';
2
+ import { sanitizedPath } from '../../shared/printable.js';
3
+
4
+ /**
5
+ * The tmp ref's admission: which tool inputs may carry one, and the refusal
6
+ * every other input answers. What the ref REACHES is `workspace/workspace-tmp.ts`'s;
7
+ * whether a tool may be HANDED one is decided here, once, by
8
+ * {@link assertTmpRefsTaken} — which {@link normalizePathArgs} runs before it
9
+ * places any path under the checkout, so no tool surface that takes a path
10
+ * can turn `__tmp__/x` into the repository path `<kbDirName>/__tmp__/x` and
11
+ * write, read or show it as if the ref had been honoured. A surface that
12
+ * resolves its paths without the normaliser (`open_page`) calls the gate
13
+ * itself.
14
+ */
15
+
16
+ /** The ref's prefix. A ref is `__tmp__/<path inside the workspace's tmp folder>`. */
17
+ export const TMP_REF_PREFIX = '__tmp__/';
18
+
19
+ /**
20
+ * Whether a tool argument is a tmp ref rather than a repository path: the
21
+ * prefix as written, or the prefix's own folder name alone (which names the
22
+ * folder, and is refused as such by the resolver). A path that only CONTAINS
23
+ * the name — `/__tmp__/x`, `knowledge-base/__tmp__/x` — is an ordinary
24
+ * repository path.
25
+ */
26
+ export function isTmpRef(value: unknown): boolean {
27
+ return typeof value === 'string' && (value === TMP_REF_PREFIX.slice(0, -1) || value.startsWith(TMP_REF_PREFIX));
28
+ }
29
+
30
+ /**
31
+ * Why a tool refuses a tmp ref in an input that is not one of the four that
32
+ * take it: the folder is read-only through the ref, and a file is kept by
33
+ * copying it into the repository. A 400 with a `kind` to branch on, before
34
+ * any gate, lock or write.
35
+ */
36
+ export function tmpRefReadOnlyRefusal(ref: string, tool: string): WorkflowValidationError {
37
+ const shown = sanitizedPath(ref);
38
+ return new WorkflowValidationError(
39
+ `"${shown}" is a tmp ref, and ${tool} does not take one there: the workspace's tmp folder is read-only through ` +
40
+ 'the ref. read_file, file_stat and grep read the one file it names; to keep the file, copy_file it to a ' +
41
+ 'repository path (the ref as `src`, the repository path as `dest`).',
42
+ { kind: 'tmp-ref-read-only', path: shown, tool },
43
+ );
44
+ }
45
+
46
+ /** The tool inputs that carry a workspace path as a string. */
47
+ const PATH_INPUTS = ['path', 'src', 'dest', 'destination'] as const;
48
+
49
+ /**
50
+ * Every path a tool call carries, with the input it came in: the string
51
+ * inputs, and for EVERY list input, each entry that is a path itself
52
+ * (`paths` of `request_file_download`) or an object carrying one in `path`
53
+ * (`files[].path` of a batch write, `resolutions[].path` of
54
+ * `update_branch`). Read by shape rather than by a list of tool inputs, so a
55
+ * list a tool adds later is gated without anyone remembering to name it here.
56
+ */
57
+ function pathEntries(args: Record<string, unknown>): Array<[string, unknown]> {
58
+ const entries: Array<[string, unknown]> = PATH_INPUTS.map((key) => [key, args[key]]);
59
+ for (const [key, value] of Object.entries(args)) {
60
+ if (!Array.isArray(value)) continue;
61
+ for (const item of value as unknown[]) {
62
+ if (typeof item === 'string') {
63
+ if (key === 'paths') entries.push([key, item]);
64
+ } else if (item && typeof item === 'object') {
65
+ entries.push([key, (item as Record<string, unknown>).path]);
66
+ }
67
+ }
68
+ }
69
+ return entries;
70
+ }
71
+
72
+ /**
73
+ * Refuse a tmp ref in any input of `tool` that is not in `takes` — with
74
+ * {@link tmpRefReadOnlyRefusal}, before anything else runs. `takes` is empty
75
+ * for every tool but the four reads of the one file a ref names.
76
+ */
77
+ export function assertTmpRefsTaken(tool: string, args: Record<string, unknown>, takes: readonly string[] = []): void {
78
+ for (const [key, value] of pathEntries(args)) {
79
+ if (isTmpRef(value) && !takes.includes(key)) throw tmpRefReadOnlyRefusal(value as string, tool);
80
+ }
81
+ }
@@ -0,0 +1,137 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { eq } from 'drizzle-orm';
4
+ import { migrate } from 'drizzle-orm/node-postgres/migrator';
5
+ import type { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
6
+ import { BevelOAuthProvider } from '../oauth/bevel-oauth-provider.js';
7
+ import { ExternalApiKeyService } from '../../tool-auth/external-api-key.service.js';
8
+ import { closeDb, createDb, type Database } from '../../database/connection.js';
9
+ import { agentConnections, externalApiKeys, oauthClients, users } from '../../database/core-schema.js';
10
+ import { coreMigrationsDir } from '../../../assets.js';
11
+ import { PG_ADMIN_URL, PG_TEST_TIMEOUT, dropScratchDatabases, scratchDatabaseUrl } from '../../../__tests__/pg-scratch.js';
12
+
13
+ /**
14
+ * The "Connect as" choice, through the real intermediary it is stored in: a
15
+ * migrated Postgres. The consent's choice goes onto the one-time code at
16
+ * Finish, crosses the token exchange onto the agent connection, and comes back
17
+ * out of every access-token verification; a key's choice goes onto its row at
18
+ * mint and comes back out of every key verification. Driven through the
19
+ * provider's and the key service's public surfaces only. Skipped without
20
+ * `TEST_DATABASE_URL`, as every `*.pg.test.ts` is.
21
+ */
22
+
23
+ const PREFIX = 'bevel-mcp_';
24
+ const REDIRECT = 'https://agent.example.com/callback';
25
+
26
+ describe.skipIf(!PG_ADMIN_URL)('connecting as an agent, on a real Postgres', () => {
27
+ let db: Database;
28
+ let provider: BevelOAuthProvider;
29
+
30
+ beforeAll(async () => {
31
+ db = createDb(await scratchDatabaseUrl('hexis_connect_as'), { piiKey: randomBytes(32).toString('base64') });
32
+ await migrate(db, { migrationsFolder: coreMigrationsDir() });
33
+ provider = new BevelOAuthProvider({
34
+ db,
35
+ crypto: null,
36
+ stateSecret: 'test-state-secret',
37
+ publicFrontendUrl: 'https://app.example.com',
38
+ tokenPrefix: PREFIX,
39
+ });
40
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
41
+ }, PG_TEST_TIMEOUT);
42
+
43
+ afterAll(async () => {
44
+ vi.restoreAllMocks();
45
+ await closeDb(db);
46
+ await dropScratchDatabases();
47
+ }, PG_TEST_TIMEOUT);
48
+
49
+ async function person(tag: string): Promise<string> {
50
+ const [user] = await db
51
+ .insert(users)
52
+ .values({ email: `${tag}@example.com`, emailBidx: `${tag}@example.com`, name: tag })
53
+ .returning({ id: users.id });
54
+ return user!.id;
55
+ }
56
+
57
+ async function registeredClient(tag: string): Promise<OAuthClientInformationFull> {
58
+ const clientId = `client-${tag}-${randomBytes(4).toString('hex')}`;
59
+ await db.insert(oauthClients).values({ clientId, clientName: 'Claude', redirectUris: [REDIRECT] });
60
+ return { client_id: clientId, redirect_uris: [REDIRECT], token_endpoint_auth_method: 'none' };
61
+ }
62
+
63
+ /** Finish on the consent step with `agentPath` (undefined: the route's Everything), then the client's exchange. */
64
+ async function consent(userId: string, client: OAuthClientInformationFull, agentPath?: string | null) {
65
+ const st = { c: client.client_id, r: REDIRECT, cc: 'challenge', iat: Date.now() };
66
+ const { redirectTo } =
67
+ agentPath === undefined ? await provider.issueAuthCode(userId, st) : await provider.issueAuthCode(userId, st, agentPath);
68
+ const code = new URL(redirectTo).searchParams.get('code')!;
69
+ return provider.exchangeAuthorizationCode(client, code);
70
+ }
71
+
72
+ const connectionsOf = (userId: string) =>
73
+ db
74
+ .select({ id: agentConnections.id, agentPath: agentConnections.agentPath, revokedAt: agentConnections.revokedAt })
75
+ .from(agentConnections)
76
+ .where(eq(agentConnections.userId, userId));
77
+
78
+ it('Everything: a consent with no choice records null and verifies with none — exactly as before', async () => {
79
+ const userId = await person('everything');
80
+ const tokens = await consent(userId, await registeredClient('everything'));
81
+ expect(await connectionsOf(userId)).toEqual([{ id: expect.any(String), agentPath: null, revokedAt: null }]);
82
+ expect((await provider.verifyAccessToken(tokens.access_token)).extra?.agentPath).toBeNull();
83
+ });
84
+
85
+ it("the consent's choice round-trips to the connection row, and out of every verification", async () => {
86
+ const userId = await person('ada');
87
+ const client = await registeredClient('ada');
88
+ const tokens = await consent(userId, client, 'Agents/delivery-mechanic.agent');
89
+
90
+ const [row] = await connectionsOf(userId);
91
+ expect(row!.agentPath).toBe('Agents/delivery-mechanic.agent');
92
+ const info = await provider.verifyAccessToken(tokens.access_token);
93
+ expect(info.extra).toMatchObject({ connectionId: row!.id, agentPath: 'Agents/delivery-mechanic.agent' });
94
+
95
+ // A refresh keeps the connection's choice.
96
+ const refreshed = await provider.exchangeRefreshToken(client, tokens.refresh_token!);
97
+ expect((await provider.verifyAccessToken(refreshed.access_token)).extra?.agentPath).toBe('Agents/delivery-mechanic.agent');
98
+ });
99
+
100
+ it('re-consenting with another choice changes the live connection, and its tokens follow it', async () => {
101
+ const userId = await person('bea');
102
+ const first = await consent(userId, await registeredClient('bea'), 'Agents/a.agent');
103
+ // Claude registers afresh on every re-authorisation; the agent is one connection.
104
+ await consent(userId, await registeredClient('bea'), null);
105
+
106
+ const rows = await connectionsOf(userId);
107
+ expect(rows).toHaveLength(1);
108
+ expect(rows[0]!.agentPath).toBeNull();
109
+ // The earlier pair still verifies — and now as the connection's choice.
110
+ expect((await provider.verifyAccessToken(first.access_token)).extra?.agentPath).toBeNull();
111
+
112
+ await consent(userId, await registeredClient('bea'), 'Agents/b.agent');
113
+ expect((await connectionsOf(userId))[0]!.agentPath).toBe('Agents/b.agent');
114
+ });
115
+
116
+ it("a key's choice is stored at mint and comes back out of every verification", async () => {
117
+ const userId = await person('cy');
118
+ const keys = new ExternalApiKeyService(db, 'bevel_');
119
+ const asAgent = await keys.mint(userId, 'pipeline', { agentPath: 'Agents/ops.agent' });
120
+ const asEverything = await keys.mint(userId, 'laptop');
121
+
122
+ expect(asAgent.summary.agentPath).toBe('Agents/ops.agent');
123
+ expect(asEverything.summary.agentPath).toBeNull();
124
+ expect((await keys.verifyAndLoadToken(asAgent.plaintext))?.agentPath).toBe('Agents/ops.agent');
125
+ expect((await keys.verifyAndLoadToken(asEverything.plaintext))?.agentPath).toBeNull();
126
+
127
+ const stored = await db
128
+ .select({ label: externalApiKeys.label, agentPath: externalApiKeys.agentPath })
129
+ .from(externalApiKeys)
130
+ .where(eq(externalApiKeys.userId, userId));
131
+ expect(stored.sort((a, b) => a.label.localeCompare(b.label))).toEqual([
132
+ { label: 'laptop', agentPath: null },
133
+ { label: 'pipeline', agentPath: 'Agents/ops.agent' },
134
+ ]);
135
+ expect((await keys.listForUser(userId)).map((k) => k.agentPath).sort()).toEqual(['Agents/ops.agent', null]);
136
+ });
137
+ });
@@ -53,8 +53,8 @@ function makeExternalApiKeyService(
53
53
 
54
54
  function makeOAuthProvider(
55
55
  verify?: (t: string) => Promise<{ extra?: Record<string, unknown> }>,
56
- /** Whether an agent connection still admits requests — true unless a test says otherwise. */
57
- live?: (connectionId: string) => Promise<boolean>,
56
+ /** A live agent connection's current choice, or null when revoked — live as Everything unless a test says otherwise. */
57
+ live?: (connectionId: string) => Promise<{ agentPath: string | null } | null>,
58
58
  ) {
59
59
  return {
60
60
  looksLikeAccessToken: (t: string) => typeof t === 'string' && t.startsWith('bevel-mcp_'),
@@ -64,7 +64,7 @@ function makeOAuthProvider(
64
64
  throw new InvalidTokenError('unknown token');
65
65
  }),
66
66
  ),
67
- isConnectionLive: vi.fn(live ?? (async () => true)),
67
+ liveConnectionChoice: vi.fn(live ?? (async () => ({ agentPath: null }))),
68
68
  noteConnectionUse: vi.fn(),
69
69
  } as unknown as BevelOAuthProvider;
70
70
  }
@@ -318,10 +318,34 @@ describe('createMcpAuthMiddleware', () => {
318
318
  expect(req.agentConnectionId).toBe('conn-7');
319
319
  // Every request through the exchanged grant asks whether the
320
320
  // connection still admits one, and counts as a use of it.
321
- expect((oauth as any).isConnectionLive).toHaveBeenCalledWith('conn-7');
321
+ expect((oauth as any).liveConnectionChoice).toHaveBeenCalledWith('conn-7');
322
322
  expect((oauth as any).noteConnectionUse).toHaveBeenCalledWith('conn-7');
323
323
  });
324
324
 
325
+ it("serves an exchanged grant as its connection's CURRENT choice, not the one it was minted with", async () => {
326
+ const auth = makeAuthService();
327
+ (auth.getUserById as ReturnType<typeof vi.fn>) = vi.fn(async () => ({
328
+ id: 'user-7',
329
+ email: 'seven@example.com',
330
+ }));
331
+ // Minted as Everything; the person has since re-consented as an agent.
332
+ const everything = internal.mint({ userId: 'user-7', externalProxy: true, connectionId: 'conn-7' }, 60_000);
333
+ const narrowed = makeOAuthProvider(undefined, async () => ({ agentPath: 'Agents/narrow.agent' }));
334
+ const first = makeReqRes(`Bearer ${everything}`);
335
+ await makeMw({ internal, auth, oauth: narrowed })(first.req, first.res, first.next);
336
+ expect(first.next).toHaveBeenCalled();
337
+ expect(first.req.mcpAgentPath).toBe('Agents/narrow.agent');
338
+ // Minted as an agent; the person has since re-consented as Everything.
339
+ const asAgent = internal.mint(
340
+ { userId: 'user-7', externalProxy: true, connectionId: 'conn-7', agentPath: 'Agents/narrow.agent' },
341
+ 60_000,
342
+ );
343
+ const second = makeReqRes(`Bearer ${asAgent}`);
344
+ await makeMw({ internal, auth, oauth: makeOAuthProvider() })(second.req, second.res, second.next);
345
+ expect(second.next).toHaveBeenCalled();
346
+ expect(second.req.mcpAgentPath).toBeUndefined();
347
+ });
348
+
325
349
  it('401s an exchanged grant whose agent connection was revoked — the token is stateless, the connection is not', async () => {
326
350
  const token = internal.mint({ userId: 'user-7', externalProxy: true, connectionId: 'conn-7' }, 60_000);
327
351
  const auth = makeAuthService();
@@ -329,7 +353,7 @@ describe('createMcpAuthMiddleware', () => {
329
353
  id: 'user-7',
330
354
  email: 'seven@example.com',
331
355
  }));
332
- const oauth = makeOAuthProvider(undefined, async () => false);
356
+ const oauth = makeOAuthProvider(undefined, async () => null);
333
357
  const mw = makeMw({ internal, auth, oauth });
334
358
  const { req, res, next, status, setHeader } = makeReqRes(`Bearer ${token}`);
335
359
  await mw(req, res, next);
@@ -29,6 +29,12 @@ import type { McpCallerKind } from '../mcp-auth.middleware.js';
29
29
  */
30
30
  export const fakeAuth: RequestHandler = (req, _res, next) => {
31
31
  req.userId = (req.headers['x-test-user'] as string | undefined) ?? 'user-A';
32
+ // The signed-in person's email, for a suite whose route reads it.
33
+ const email = req.headers['x-test-email'] as string | undefined;
34
+ if (email) req.userEmail = email;
35
+ // The agent the credential connects as, as the real middleware sets it.
36
+ const agentPath = req.headers['x-test-agent-path'] as string | undefined;
37
+ if (agentPath) req.mcpAgentPath = agentPath;
32
38
  // The credential kind the real middleware would have recognised, when a
33
39
  // suite needs one (the page header is a session's alone).
34
40
  const kind = req.headers['x-test-kind'] as McpCallerKind | undefined;
@@ -48,6 +54,7 @@ export interface McpRoutesDeps {
48
54
  internalTokens?: unknown;
49
55
  oauthProvider?: unknown;
50
56
  resourceMetadataUrl?: string;
57
+ agents?: unknown;
51
58
  }
52
59
 
53
60
  /** Servers opened by `mountMcpRoutes`, closed together by `closeMountedRoutes`. */
@@ -73,6 +80,7 @@ export async function mountMcpRoutes(deps: McpRoutesDeps = {}): Promise<string>
73
80
  (deps.internalTokens ?? stub) as never,
74
81
  (deps.oauthProvider ?? stub) as never,
75
82
  deps.resourceMetadataUrl ?? '',
83
+ deps.agents as never,
76
84
  ),
77
85
  );
78
86
  const server = await new Promise<HttpServer>((resolve) => {