@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
@@ -0,0 +1,195 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import express from 'express';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { CONNECTION_META_KEY, McpService, type McpCaller } from '../mcp.service.js';
9
+ import { SpillStore } from '../../workspace/index.js';
10
+ import { createManualRoutes } from '../../tool-registry/manual.routes.js';
11
+ import { ToolRegistry } from '../../tool-registry/tool-registry.js';
12
+ import { toolDef } from '../../tool-helpers/index.js';
13
+ import type { ConnectionScopeWire } from '../../agent-scope/index.js';
14
+
15
+ /**
16
+ * The MCP proxy's half of connecting as an agent. The surfaces behind the
17
+ * loopback do the narrowing (pinned in the agent-scope suites); what the proxy
18
+ * owns is the WORDING and the CACHE: a tool off a narrowed surface is refused
19
+ * by name and agent rather than as unknown, a connection whose file is gone is
20
+ * served an empty listing with the reason, the handshake says what the
21
+ * connection is connected as, and two connections of one person made as
22
+ * different agents never share a built surface.
23
+ *
24
+ * The loopback here answers each bearer as the agent surfaces would answer
25
+ * that credential: `bevel_all` is Everything, `bevel_narrow` an agent that
26
+ * loads `read_file` alone, `bevel_gone` one whose file was deleted.
27
+ */
28
+
29
+ const NARROW: ConnectionScopeWire = { agent: { path: 'Agents/narrow.agent', name: 'narrow' } };
30
+ const GONE: ConnectionScopeWire = {
31
+ agent: { path: 'Agents/gone.agent', name: 'Agents/gone.agent' },
32
+ unavailable: 'This connection was made as the agent file `Agents/gone.agent`, which no longer exists.',
33
+ };
34
+ const CONNECTIONS: Record<string, ConnectionScopeWire | undefined> = {
35
+ bevel_all: undefined,
36
+ bevel_narrow: NARROW,
37
+ bevel_gone: GONE,
38
+ };
39
+
40
+ let httpServer: HttpServer | undefined;
41
+ const cleanups: Array<() => Promise<void>> = [];
42
+ afterEach(async () => {
43
+ for (const c of cleanups.splice(0)) await c().catch(() => {});
44
+ if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
45
+ httpServer = undefined;
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ async function harness() {
50
+ const state = { builds: 0, base: '' };
51
+ const connectionOf = (req: express.Request) => CONNECTIONS[(req.headers.authorization ?? '').replace(/^Bearer /, '')];
52
+ const registry = new ToolRegistry();
53
+ for (const name of ['read_file', 'edit_file']) {
54
+ registry.registerExternalTool(
55
+ toolDef({ name, description: name, path: `/api/agent/tools/${name}`, inputs: { type: 'object', properties: {} } }),
56
+ );
57
+ }
58
+ const app = express();
59
+ app.use(express.json());
60
+ app.get('/api/agent/catalog-revision', (req, res) => {
61
+ const connection = connectionOf(req);
62
+ res.json({ revision: 'rev-1', tools: 0, skills: 0, ...(connection ? { connection } : {}) });
63
+ });
64
+ app.get('/api/agent/all-tools', (req, res) => {
65
+ state.builds += 1;
66
+ const connection = connectionOf(req);
67
+ res.json({ manuals: [], ...(connection ? { connection } : {}) });
68
+ });
69
+ // The platform manual, as the agent surfaces narrow it per credential.
70
+ app.get('/api/agent/utcp', (req, res, next) => {
71
+ const connection = connectionOf(req);
72
+ if (!connection) return next();
73
+ res.json({
74
+ utcp_version: '1.1.0',
75
+ manual_version: '1.0.0',
76
+ tools: connection.unavailable
77
+ ? []
78
+ : [
79
+ {
80
+ name: 'read_file',
81
+ description: 'read_file',
82
+ inputs: { type: 'object', properties: {} },
83
+ outputs: { type: 'object', properties: {} },
84
+ tags: [],
85
+ tool_call_template: {
86
+ call_template_type: 'http',
87
+ http_method: 'POST',
88
+ url: `${state.base}/api/agent/tools/read_file`,
89
+ content_type: 'application/json',
90
+ },
91
+ },
92
+ ],
93
+ });
94
+ });
95
+ app.use('/api', createManualRoutes(registry, (_req, _res, next) => next()));
96
+ httpServer = await new Promise<HttpServer>((r) => {
97
+ const s = app.listen(0, () => r(s));
98
+ });
99
+ state.base = `http://127.0.0.1:${(httpServer.address() as { port: number }).port}`;
100
+ const mcp = new McpService({
101
+ loopbackBaseUrl: state.base,
102
+ manualName: 'KNOWLEDGE_BASE',
103
+ spillStore: new SpillStore(join(tmpdir(), 'bevel-test-spills')),
104
+ publicFrontendUrl: 'http://localhost:5173',
105
+ });
106
+ return { state, mcp };
107
+ }
108
+
109
+ async function connect(mcp: McpService, caller: Partial<McpCaller>, initialize = false): Promise<Client> {
110
+ const message = initialize
111
+ ? { jsonrpc: '2.0', id: 0, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 't', version: '0' } } }
112
+ : { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} };
113
+ const server = await mcp.createRequestServer(
114
+ { userId: 'user-A', tokenId: 'key-1', connectionId: null, bearer: 'bevel_all', ...caller },
115
+ message,
116
+ );
117
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
118
+ await server.connect(serverTransport);
119
+ const client = new Client({ name: 'test-client', version: '0.0.0' }, { capabilities: {} });
120
+ await client.connect(clientTransport);
121
+ cleanups.push(async () => {
122
+ await client.close();
123
+ await server.close();
124
+ });
125
+ return client;
126
+ }
127
+
128
+ const textOf = (res: Awaited<ReturnType<Client['callTool']>>) =>
129
+ (res.content as { type: string; text?: string }[]).find((c) => c.type === 'text')?.text;
130
+
131
+ describe('a narrowed connection', () => {
132
+ it('lists what its agent loads, and refuses another tool by name and agent — not as unknown', async () => {
133
+ const { mcp } = await harness();
134
+ vi.spyOn(console, 'log').mockImplementation(() => {});
135
+ const client = await connect(mcp, { bearer: 'bevel_narrow', agentPath: 'Agents/narrow.agent' });
136
+ const names = (await client.listTools()).tools.map((t) => t.name);
137
+ expect(names).toContain('read_file');
138
+ expect(names).not.toContain('edit_file');
139
+ const refused = await client.callTool({ name: 'edit_file', arguments: {} });
140
+ expect(refused.isError).toBe(true);
141
+ expect(textOf(refused)).toBe(
142
+ 'The tool "edit_file" is not available to this connection: it is connected as the agent "narrow" ' +
143
+ '(`Agents/narrow.agent`), which does not load it.',
144
+ );
145
+ });
146
+
147
+ it('Everything still answers an unknown tool as unknown', async () => {
148
+ const { mcp } = await harness();
149
+ vi.spyOn(console, 'log').mockImplementation(() => {});
150
+ const client = await connect(mcp, {});
151
+ expect((await client.listTools()).tools.map((t) => t.name)).toEqual(
152
+ expect.arrayContaining(['read_file', 'edit_file']),
153
+ );
154
+ expect(textOf(await client.callTool({ name: 'nope', arguments: {} }))).toBe('Unknown tool "nope".');
155
+ });
156
+
157
+ it('tells the agent at the handshake what it is connected as; Everything is told what it always was', async () => {
158
+ const { mcp } = await harness();
159
+ vi.spyOn(console, 'log').mockImplementation(() => {});
160
+ const narrowed = (await connect(mcp, { bearer: 'bevel_narrow', agentPath: 'Agents/narrow.agent' }, true)).getInstructions();
161
+ const everything = (await connect(mcp, {}, true)).getInstructions();
162
+ expect(narrowed).toBe(
163
+ `${everything}\n\nThis connection is connected as the agent "narrow" (\`Agents/narrow.agent\`): it loads only the ` +
164
+ 'platform tools, connectors, skills and plugins that file lists, and of those only what you may read.',
165
+ );
166
+ expect(everything).not.toContain('connected as the agent');
167
+ });
168
+
169
+ it('two connections of one person made as different agents never share a built surface', async () => {
170
+ const { state, mcp } = await harness();
171
+ vi.spyOn(console, 'log').mockImplementation(() => {});
172
+ await (await connect(mcp, { bearer: 'bevel_narrow', agentPath: 'Agents/narrow.agent' })).listTools();
173
+ await (await connect(mcp, { bearer: 'bevel_narrow', agentPath: 'Agents/other.agent' })).listTools();
174
+ await (await connect(mcp, { bearer: 'bevel_narrow', agentPath: 'Agents/narrow.agent' })).listTools();
175
+ expect(state.builds).toBe(2);
176
+ });
177
+ });
178
+
179
+ describe('a connection whose agent file is gone', () => {
180
+ it('is served an empty listing carrying the reason, and every call — meta-tools too — answers it', async () => {
181
+ const { mcp } = await harness();
182
+ vi.spyOn(console, 'log').mockImplementation(() => {});
183
+ const client = await connect(mcp, { bearer: 'bevel_gone', agentPath: 'Agents/gone.agent' });
184
+ const listed = await client.listTools();
185
+ expect(listed.tools).toEqual([]);
186
+ expect(listed._meta).toEqual({ [CONNECTION_META_KEY]: GONE.unavailable });
187
+ for (const name of ['read_file', 'call_tool_chain']) {
188
+ const res = await client.callTool({ name, arguments: {} });
189
+ expect(res.isError).toBe(true);
190
+ expect(textOf(res)).toBe(GONE.unavailable);
191
+ }
192
+ const instructions = (await connect(mcp, { bearer: 'bevel_gone', agentPath: 'Agents/gone.agent' }, true)).getInstructions();
193
+ expect(instructions?.endsWith(GONE.unavailable!)).toBe(true);
194
+ });
195
+ });
@@ -0,0 +1,192 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { InternalTokenService } from '../../tool-auth/internal-token.service.js';
3
+ import { createTokenVerifier } from '../../tool-auth/tool-auth.middleware.js';
4
+ import type { IExternalApiKeyService } from '../../tool-auth/external-api-key.interface.js';
5
+ import type { BevelOAuthProvider } from '../oauth/bevel-oauth-provider.js';
6
+ import { NOT_AN_AGENT_CHOICE } from '../oauth/oauth-consent.routes.js';
7
+ import { agentFile, agentScopeOver, inMemoryKb } from '../../agent-scope/__tests__/in-memory-kb.js';
8
+ import { closeMountedRoutes, mountMcpRoutes } from './mcp-routes-harness.js';
9
+
10
+ /**
11
+ * Connecting as an agent on the MCP router: the key-mint path takes the same
12
+ * "Connect as" choice the consent step does, the dialog is told the same
13
+ * agents, the local server's exchanged grant carries the connection's agent,
14
+ * and every MCP request hands the proxy the agent its credential connects as.
15
+ */
16
+
17
+ const ADA = 'ada@example.com';
18
+ const kb = inMemoryKb({
19
+ 'Agents/ops.agent': agentFile('name: ops\ndescription: Watches the dashboards.'),
20
+ 'Agents/secret.agent': agentFile('name: secret'),
21
+ });
22
+ kb.hidden.add('Agents/secret.agent');
23
+
24
+ afterEach(async () => {
25
+ await closeMountedRoutes();
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ function keyService() {
30
+ const mint = vi.fn(async (_userId: string, label: string, options?: { agentPath?: string }) => ({
31
+ plaintext: 'bevel_new',
32
+ summary: { id: 'k1', label, agentPath: options?.agentPath ?? null },
33
+ }));
34
+ return {
35
+ service: { looksLikeExternalApiKey: (t: string) => t.startsWith('bevel_'), mint } as unknown as IExternalApiKeyService,
36
+ mint,
37
+ };
38
+ }
39
+
40
+ const post = (url: string, body: unknown) =>
41
+ fetch(url, {
42
+ method: 'POST',
43
+ headers: { 'content-type': 'application/json', 'x-test-email': ADA },
44
+ body: JSON.stringify(body),
45
+ });
46
+
47
+ describe('minting a connection key', () => {
48
+ it('as Everything: no choice is minted exactly as before', async () => {
49
+ const { service, mint } = keyService();
50
+ const base = await mountMcpRoutes({ externalApiKeyService: service, agents: agentScopeOver(kb) });
51
+ const res = await post(`${base}/api/mcp/external-api-keys`, { label: 'laptop' });
52
+ expect(res.status).toBe(200);
53
+ expect(mint).toHaveBeenCalledWith('user-A', 'laptop', {});
54
+ });
55
+
56
+ it('as an agent on offer: the key records its normalized path', async () => {
57
+ const { service, mint } = keyService();
58
+ const base = await mountMcpRoutes({ externalApiKeyService: service, agents: agentScopeOver(kb) });
59
+ const res = await post(`${base}/api/mcp/external-api-keys`, { label: 'pipeline', agentPath: 'Agents/ops.agent/' });
60
+ expect(res.status).toBe(200);
61
+ expect(mint).toHaveBeenCalledWith('user-A', 'pipeline', { agentPath: 'Agents/ops.agent' });
62
+ expect(((await res.json()) as { summary: { agentPath: string } }).summary.agentPath).toBe('Agents/ops.agent');
63
+ });
64
+
65
+ it.each([['Agents/secret.agent'], ['Agents/none.agent'], [7]])('refuses %s, minting nothing', async (agentPath) => {
66
+ const { service, mint } = keyService();
67
+ const base = await mountMcpRoutes({ externalApiKeyService: service, agents: agentScopeOver(kb) });
68
+ const res = await post(`${base}/api/mcp/external-api-keys`, { label: 'x', agentPath });
69
+ expect(res.status).toBe(400);
70
+ expect(await res.json()).toEqual({ error: NOT_AN_AGENT_CHOICE });
71
+ expect(mint).not.toHaveBeenCalled();
72
+ });
73
+
74
+ it('offers the same agents the consent step does', async () => {
75
+ const base = await mountMcpRoutes({ agents: agentScopeOver(kb) });
76
+ const res = await fetch(`${base}/api/mcp/agent-choices`, { headers: { 'x-test-email': ADA } });
77
+ expect(await res.json()).toEqual({
78
+ agents: [{ path: 'Agents/ops.agent', name: 'ops', description: 'Watches the dashboards.' }],
79
+ });
80
+ });
81
+
82
+ it('answers a failed listing as a failure, never as "no agents"', async () => {
83
+ const failing = { listChoices: async () => Promise.reject(new Error('clone failed')), choosable: async () => null };
84
+ const base = await mountMcpRoutes({ agents: failing });
85
+ const res = await fetch(`${base}/api/mcp/agent-choices`, { headers: { 'x-test-email': ADA } });
86
+ expect(res.status).toBe(500);
87
+ expect(await res.json()).not.toHaveProperty('agents');
88
+ });
89
+ });
90
+
91
+ describe('the local server’s exchanged grant', () => {
92
+ it("carries the connection's agent, so its calls narrow exactly as the hosted endpoint's", async () => {
93
+ const internalTokens = new InternalTokenService({ secret: 'test-secret' });
94
+ const oauth = {
95
+ looksLikeAccessToken: (t: string) => t.startsWith('bevel-mcp_'),
96
+ verifyAccessToken: async () => ({
97
+ extra: { userId: 'user-5', userEmail: ADA, connectionId: 'conn-9', agentPath: 'Agents/ops.agent' },
98
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
99
+ }),
100
+ } as unknown as BevelOAuthProvider;
101
+ const base = await mountMcpRoutes({
102
+ externalApiKeyService: { looksLikeExternalApiKey: () => false } as never,
103
+ internalTokens,
104
+ oauthProvider: oauth,
105
+ });
106
+ const res = await fetch(`${base}/api/mcp/local-token`, {
107
+ method: 'POST',
108
+ headers: { Authorization: 'Bearer bevel-mcp_grant' },
109
+ });
110
+ const { token } = (await res.json()) as { token: string };
111
+ const verify = createTokenVerifier({ looksLikeExternalApiKey: () => false } as never, internalTokens);
112
+ expect(await verify(token)).toEqual({
113
+ ok: true,
114
+ auth: { source: 'external', userId: 'user-5', connectionId: 'conn-9', agentPath: 'Agents/ops.agent', scope: 'write' },
115
+ });
116
+ });
117
+
118
+ it('an Everything grant mints the token it always did', async () => {
119
+ const internalTokens = new InternalTokenService({ secret: 'test-secret' });
120
+ const oauth = {
121
+ looksLikeAccessToken: (t: string) => t.startsWith('bevel-mcp_'),
122
+ verifyAccessToken: async () => ({
123
+ extra: { userId: 'user-5', userEmail: ADA, connectionId: 'conn-9', agentPath: null },
124
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
125
+ }),
126
+ } as unknown as BevelOAuthProvider;
127
+ const base = await mountMcpRoutes({
128
+ externalApiKeyService: { looksLikeExternalApiKey: () => false } as never,
129
+ internalTokens,
130
+ oauthProvider: oauth,
131
+ });
132
+ const res = await fetch(`${base}/api/mcp/local-token`, { method: 'POST', headers: { Authorization: 'Bearer bevel-mcp_g' } });
133
+ const { token } = (await res.json()) as { token: string };
134
+ expect(internalTokens.verify(token)).toEqual({ userId: 'user-5', externalProxy: true, connectionId: 'conn-9' });
135
+ });
136
+
137
+ it("a direct tool call reads the connection's CURRENT choice: a re-consent reaches a token minted before it", async () => {
138
+ const internalTokens = new InternalTokenService({ secret: 'test-secret' });
139
+ const keys = { looksLikeExternalApiKey: () => false } as never;
140
+ let current: { agentPath: string | null } | null = { agentPath: 'Agents/ops.agent' };
141
+ const verify = createTokenVerifier(keys, internalTokens, undefined, {
142
+ liveConnectionChoice: async (id) => (id === 'conn-9' ? current : null),
143
+ });
144
+ // Minted as Everything, served as the agent chosen since.
145
+ const everything = internalTokens.mint({ userId: 'user-5', externalProxy: true, connectionId: 'conn-9' }, 60_000);
146
+ expect(await verify(everything)).toEqual({
147
+ ok: true,
148
+ auth: { source: 'external', userId: 'user-5', connectionId: 'conn-9', agentPath: 'Agents/ops.agent', scope: 'write' },
149
+ });
150
+ // Minted as an agent, served as Everything once the person chose it.
151
+ current = { agentPath: null };
152
+ const asAgent = internalTokens.mint(
153
+ { userId: 'user-5', externalProxy: true, connectionId: 'conn-9', agentPath: 'Agents/ops.agent' },
154
+ 60_000,
155
+ );
156
+ expect(await verify(asAgent)).toEqual({
157
+ ok: true,
158
+ auth: { source: 'external', userId: 'user-5', connectionId: 'conn-9', scope: 'write' },
159
+ });
160
+ });
161
+
162
+ it("refuses a direct tool call through a revoked connection's token, whatever the token says it connects as", async () => {
163
+ const internalTokens = new InternalTokenService({ secret: 'test-secret' });
164
+ const keys = { looksLikeExternalApiKey: () => false } as never;
165
+ const verify = createTokenVerifier(keys, internalTokens, undefined, { liveConnectionChoice: async () => null });
166
+ const token = internalTokens.mint(
167
+ { userId: 'user-5', externalProxy: true, connectionId: 'conn-9', agentPath: 'Agents/ops.agent' },
168
+ 60_000,
169
+ );
170
+ expect(await verify(token)).toEqual({ ok: false, status: 401, message: 'Access for this agent was revoked' });
171
+ });
172
+ });
173
+
174
+ describe('an MCP request', () => {
175
+ it("hands the proxy the agent its credential connects as — and nothing for Everything", async () => {
176
+ const createRequestServer = vi.fn(async () => {
177
+ throw new Error('stop here');
178
+ });
179
+ const base = await mountMcpRoutes({ mcpService: { createRequestServer } });
180
+ const call = (headers: Record<string, string>) =>
181
+ fetch(`${base}/api/mcp`, {
182
+ method: 'POST',
183
+ headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...headers },
184
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
185
+ });
186
+ await call({ 'x-test-agent-path': 'Agents/ops.agent' });
187
+ await call({});
188
+ const callers = createRequestServer.mock.calls.map((c) => (c as unknown[])[0] as Record<string, unknown>);
189
+ expect(callers[0]).toMatchObject({ userId: 'user-A', agentPath: 'Agents/ops.agent' });
190
+ expect(callers[1]).not.toHaveProperty('agentPath');
191
+ });
192
+ });
@@ -0,0 +1,146 @@
1
+ import express from 'express';
2
+ import type { AddressInfo } from 'node:net';
3
+ import type { Server } from 'node:http';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { createOAuthConsentRoutes, NOT_AN_AGENT_CHOICE } from '../oauth/oauth-consent.routes.js';
6
+ import { signAuthRequest } from '../oauth/oauth-state.js';
7
+ import { agentFile, agentScopeOver, inMemoryKb } from '../../agent-scope/__tests__/in-memory-kb.js';
8
+
9
+ /**
10
+ * The consent step's "Connect as" choice, through the two routes the
11
+ * /connect page calls: the request description offers the agents the person
12
+ * may read, and Finish hands the provider exactly the choice made — null for
13
+ * Everything — or refuses one that is not on offer, before any code is minted.
14
+ * The choice offered and checked is the REAL agent-scope service's, over a
15
+ * knowledge base held in memory.
16
+ */
17
+
18
+ const SECRET = 'state-secret';
19
+ const ADA = 'ada@example.com';
20
+
21
+ let http: Server | null = null;
22
+ afterEach(async () => {
23
+ if (http) await new Promise<void>((r) => http!.close(() => r()));
24
+ http = null;
25
+ });
26
+
27
+ const kb = inMemoryKb({
28
+ 'Agents/mechanic.agent': agentFile('name: delivery-mechanic\ndescription: Moves code around.'),
29
+ 'Agents/secret.agent': agentFile('name: secret'),
30
+ });
31
+ kb.hidden.add('Agents/secret.agent');
32
+
33
+ async function mount(opts: { email?: string; agents?: Parameters<typeof createOAuthConsentRoutes>[0]['agents'] } = {}) {
34
+ const issueAuthCode = vi.fn(async () => ({ redirectTo: 'https://agent.example.com/cb?code=c1' }));
35
+ const completeConsent = vi.fn(async () => ({ redirectTo: 'https://claude.ai/cb?code=gh' }));
36
+ const app = express();
37
+ app.use(express.json());
38
+ app.use((req, _res, next) => {
39
+ req.userId = 'u1';
40
+ req.userEmail = opts.email ?? ADA;
41
+ next();
42
+ });
43
+ app.use(
44
+ createOAuthConsentRoutes({
45
+ provider: {
46
+ issueAuthCode,
47
+ clientsStore: { getClient: async () => ({ client_name: 'Claude' }) },
48
+ } as never,
49
+ stateSecret: SECRET,
50
+ facade: {
51
+ isFacadeRequest: () => true,
52
+ clientNameFor: () => 'Claude',
53
+ completeConsent,
54
+ },
55
+ agents: opts.agents ?? agentScopeOver(kb),
56
+ }),
57
+ );
58
+ http = app.listen(0);
59
+ await new Promise<void>((r) => http!.once('listening', r));
60
+ const base = `http://127.0.0.1:${(http.address() as AddressInfo).port}`;
61
+ return { base, issueAuthCode, completeConsent };
62
+ }
63
+
64
+ const state = (extra: Record<string, unknown> = {}) =>
65
+ signAuthRequest(SECRET, { c: 'client-1', r: 'https://agent.example.com/cb', cc: 'challenge', ...extra });
66
+
67
+ async function finish(base: string, body: Record<string, unknown>) {
68
+ return fetch(`${base}/mcp/oauth/complete`, {
69
+ method: 'POST',
70
+ headers: { 'content-type': 'application/json' },
71
+ body: JSON.stringify(body),
72
+ });
73
+ }
74
+
75
+ describe('the consent step offers "Connect as"', () => {
76
+ it('lists the agents the person may read, by name with description and path — Everything is the page’s own', async () => {
77
+ const { base } = await mount();
78
+ const res = await fetch(`${base}/mcp/oauth/request?state=${encodeURIComponent(state())}`);
79
+ expect(await res.json()).toEqual({
80
+ clientName: 'Claude',
81
+ scope: null,
82
+ resource: null,
83
+ agents: [{ path: 'Agents/mechanic.agent', name: 'delivery-mechanic', description: 'Moves code around.' }],
84
+ });
85
+ });
86
+
87
+ it('says so when the agents cannot be listed, rather than passing it off as having none', async () => {
88
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
89
+ const failing = {
90
+ listChoices: async () => {
91
+ throw new Error('checkout unavailable');
92
+ },
93
+ choosable: async () => null,
94
+ };
95
+ const { base } = await mount({ agents: failing });
96
+ const res = await fetch(`${base}/mcp/oauth/request?state=${encodeURIComponent(state())}`);
97
+ expect(await res.json()).toEqual({
98
+ clientName: 'Claude',
99
+ scope: null,
100
+ resource: null,
101
+ agents: [],
102
+ agentsUnavailable: true,
103
+ });
104
+ });
105
+ });
106
+
107
+ describe('Finish records the choice', () => {
108
+ it('Everything — no choice, null or empty — reaches the provider as null, exactly as before', async () => {
109
+ const { base, issueAuthCode } = await mount();
110
+ for (const body of [{}, { agentPath: null }, { agentPath: '' }]) {
111
+ const st = state();
112
+ const res = await finish(base, { state: st, ...body });
113
+ expect(res.status).toBe(200);
114
+ expect(await res.json()).toEqual({ redirectTo: 'https://agent.example.com/cb?code=c1' });
115
+ }
116
+ expect(issueAuthCode).toHaveBeenCalledTimes(3);
117
+ for (const call of issueAuthCode.mock.calls as unknown as unknown[][]) expect(call[2]).toBeNull();
118
+ });
119
+
120
+ it('an agent on offer reaches the provider as its normalized path', async () => {
121
+ const { base, issueAuthCode } = await mount();
122
+ const res = await finish(base, { state: state(), agentPath: '/Agents/mechanic.agent' });
123
+ expect(res.status).toBe(200);
124
+ expect(issueAuthCode).toHaveBeenCalledWith('u1', expect.objectContaining({ c: 'client-1' }), 'Agents/mechanic.agent');
125
+ });
126
+
127
+ it.each([
128
+ ['a file the person may not read', 'Agents/secret.agent'],
129
+ ['a file that does not exist', 'Agents/none.agent'],
130
+ ['a path that is not an agent', 'notes.md'],
131
+ ['a value that is not a path', 42],
132
+ ])('refuses %s with one answer, minting nothing', async (_what, agentPath) => {
133
+ const { base, issueAuthCode } = await mount();
134
+ const res = await finish(base, { state: state(), agentPath });
135
+ expect(res.status).toBe(400);
136
+ expect(await res.json()).toEqual({ error: NOT_AN_AGENT_CHOICE });
137
+ expect(issueAuthCode).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('refuses a choice on an account linked through the GitHub facade, which has no connection to record it on', async () => {
141
+ const { base, completeConsent } = await mount();
142
+ const res = await finish(base, { state: state({ gh: true, cc: undefined }), agentPath: 'Agents/mechanic.agent' });
143
+ expect(res.status).toBe(400);
144
+ expect(completeConsent).not.toHaveBeenCalled();
145
+ });
146
+ });
@@ -24,6 +24,13 @@ declare global {
24
24
  * the page its call comes from (`X-Bevel-Page`).
25
25
  */
26
26
  mcpCallerKind?: McpCallerKind;
27
+ /**
28
+ * The agent configuration the MCP caller's credential connects as — a key's
29
+ * `api_tokens.agent_path`, an agent connection's
30
+ * `agent_connections.agent_path`, or the one an exchanged grant
31
+ * carries. Unset is Everything.
32
+ */
33
+ mcpAgentPath?: string;
27
34
  }
28
35
  }
29
36
  }
@@ -110,6 +117,7 @@ export function createMcpAuthMiddleware(
110
117
  req.userId = resolved.user.id;
111
118
  req.userEmail = resolved.user.email;
112
119
  req.externalApiKeyId = resolved.tokenId;
120
+ if (resolved.agentPath) req.mcpAgentPath = resolved.agentPath;
113
121
  req.mcpCallerKind = 'key';
114
122
  next();
115
123
  return;
@@ -137,6 +145,9 @@ export function createMcpAuthMiddleware(
137
145
  if (typeof info.extra?.connectionId === 'string') {
138
146
  req.agentConnectionId = info.extra.connectionId;
139
147
  }
148
+ if (typeof info.extra?.agentPath === 'string' && info.extra.agentPath) {
149
+ req.mcpAgentPath = info.extra.agentPath;
150
+ }
140
151
  if (!req.userId) {
141
152
  unauthorized(res, 'Invalid access token');
142
153
  return;
@@ -217,10 +228,13 @@ export function createMcpAuthMiddleware(
217
228
  // stamped too (throttled, off the request's path), so the Audit log's
218
229
  // "last used" follows the local server's calls, not only its sign-ins.
219
230
  // Carried on the request so those calls are logged under the agent.
231
+ // The same read answers what the connection is made as NOW: a
232
+ // re-consent changes the row, not the token, so the row's choice is
233
+ // the one served, never the one the token was minted with.
220
234
  if (claim.connectionId) {
221
- let live: boolean;
235
+ let live: { agentPath: string | null } | null;
222
236
  try {
223
- live = await oauthProvider.isConnectionLive(claim.connectionId);
237
+ live = await oauthProvider.liveConnectionChoice(claim.connectionId);
224
238
  } catch (err) {
225
239
  log.error('agent-connection check failed:', { err });
226
240
  res.status(500).json({ error: 'Authentication backend unavailable' });
@@ -232,6 +246,9 @@ export function createMcpAuthMiddleware(
232
246
  }
233
247
  oauthProvider.noteConnectionUse(claim.connectionId);
234
248
  req.agentConnectionId = claim.connectionId;
249
+ if (live.agentPath) req.mcpAgentPath = live.agentPath;
250
+ } else if (claim.agentPath) {
251
+ req.mcpAgentPath = claim.agentPath;
235
252
  }
236
253
  req.mcpCallerKind = 'internal';
237
254
  next();