@bevel-software/platform-core-backend 0.13.6 → 0.15.1

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 (456) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +101 -9
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +20 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +105 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +12 -5
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +18 -6
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +26 -1
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +213 -71
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access.routes.d.ts.map +1 -1
  23. package/dist/modules/access/access.routes.js +59 -7
  24. package/dist/modules/access/access.routes.js.map +1 -1
  25. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  26. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  27. package/dist/modules/access-model/access-grammar.js +95 -1
  28. package/dist/modules/access-model/access-grammar.js.map +1 -1
  29. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  30. package/dist/modules/access-model/group-files.js +4 -1
  31. package/dist/modules/access-model/group-files.js.map +1 -1
  32. package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
  33. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
  34. package/dist/modules/access-model/kb-read-filter.js +15 -0
  35. package/dist/modules/access-model/kb-read-filter.js.map +1 -1
  36. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  37. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  38. package/dist/modules/access-model/plugin-principals.js +125 -0
  39. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  40. package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
  41. package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
  42. package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
  43. package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
  44. package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
  45. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
  46. package/dist/modules/connection-probe/connection-probe.service.js +684 -0
  47. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
  48. package/dist/modules/connection-probe/index.d.ts +3 -0
  49. package/dist/modules/connection-probe/index.d.ts.map +1 -0
  50. package/dist/modules/connection-probe/index.js +3 -0
  51. package/dist/modules/connection-probe/index.js.map +1 -0
  52. package/dist/modules/database/core-schema.d.ts +326 -0
  53. package/dist/modules/database/core-schema.d.ts.map +1 -1
  54. package/dist/modules/database/core-schema.js +52 -0
  55. package/dist/modules/database/core-schema.js.map +1 -1
  56. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  57. package/dist/modules/diff/diff.routes.js +3 -5
  58. package/dist/modules/diff/diff.routes.js.map +1 -1
  59. package/dist/modules/kb-fs/mutex.d.ts +37 -0
  60. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/mutex.js +48 -5
  62. package/dist/modules/kb-fs/mutex.js.map +1 -1
  63. package/dist/modules/kb-sync/index.d.ts +6 -0
  64. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  65. package/dist/modules/kb-sync/index.js +5 -0
  66. package/dist/modules/kb-sync/index.js.map +1 -0
  67. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  68. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  69. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  70. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  71. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  72. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  73. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  74. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  75. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  76. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  77. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  78. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  79. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  80. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  81. package/dist/modules/kb-sync/sync-auth.js +62 -0
  82. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  83. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  84. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  85. package/dist/modules/kb-sync/sync-payload.js +105 -0
  86. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  87. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  88. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  89. package/dist/modules/marketplace/git-http.routes.js +190 -0
  90. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  91. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  92. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  93. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  94. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  95. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  96. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  97. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  98. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  99. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  100. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  101. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  102. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  103. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  104. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  105. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  106. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  107. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  108. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  109. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  110. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  111. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  112. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  113. package/dist/modules/marketplace/github-facade/index.js +6 -0
  114. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  115. package/dist/modules/marketplace/index.d.ts +4 -0
  116. package/dist/modules/marketplace/index.d.ts.map +1 -0
  117. package/dist/modules/marketplace/index.js +4 -0
  118. package/dist/modules/marketplace/index.js.map +1 -0
  119. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  120. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  121. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  122. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  123. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  124. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  125. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  126. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  127. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  128. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  129. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  130. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  131. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  132. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  133. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  134. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  135. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  136. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  137. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  138. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  139. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  140. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  141. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  142. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  143. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  144. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  145. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  146. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  147. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  148. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  149. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  150. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  151. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  152. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  153. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  154. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  155. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  156. package/dist/modules/plugins/discovery/native.source.js +102 -0
  157. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  158. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  159. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  160. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  161. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  162. package/dist/modules/plugins/index.d.ts +10 -1
  163. package/dist/modules/plugins/index.d.ts.map +1 -1
  164. package/dist/modules/plugins/index.js +8 -0
  165. package/dist/modules/plugins/index.js.map +1 -1
  166. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  167. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  168. package/dist/modules/plugins/plugin-links.js +160 -0
  169. package/dist/modules/plugins/plugin-links.js.map +1 -0
  170. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  171. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  172. package/dist/modules/plugins/plugin-links.service.js +304 -0
  173. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  174. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  175. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  177. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  178. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  179. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  180. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  181. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  182. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  183. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  184. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  185. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  186. package/dist/modules/plugins/plugins.routes.js +120 -12
  187. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  188. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  189. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  190. package/dist/modules/plugins/plugins.service.js +108 -47
  191. package/dist/modules/plugins/plugins.service.js.map +1 -1
  192. package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
  193. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  194. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
  195. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  196. package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
  197. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  198. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  199. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  200. package/dist/modules/settings/deployment-settings.service.js +82 -2
  201. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  202. package/dist/modules/settings/setup.routes.d.ts +26 -0
  203. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  204. package/dist/modules/settings/setup.routes.js +9 -1
  205. package/dist/modules/settings/setup.routes.js.map +1 -1
  206. package/dist/modules/skills/index.d.ts +1 -0
  207. package/dist/modules/skills/index.d.ts.map +1 -1
  208. package/dist/modules/skills/index.js +1 -0
  209. package/dist/modules/skills/index.js.map +1 -1
  210. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  211. package/dist/modules/skills/pending-skills.service.js +9 -4
  212. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  213. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  214. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  215. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  216. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  217. package/dist/modules/skills/skills.contract.d.ts +31 -0
  218. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  219. package/dist/modules/skills/skills.routes.d.ts +28 -3
  220. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  221. package/dist/modules/skills/skills.routes.js +42 -3
  222. package/dist/modules/skills/skills.routes.js.map +1 -1
  223. package/dist/modules/skills/skills.service.d.ts +4 -0
  224. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  225. package/dist/modules/skills/skills.service.js +83 -22
  226. package/dist/modules/skills/skills.service.js.map +1 -1
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  228. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  229. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  230. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  232. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  233. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  234. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  235. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  236. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
  238. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  239. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  240. package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
  241. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  242. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
  243. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
  245. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  246. package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
  247. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  248. package/dist/modules/workflow/git/git.service.d.ts +58 -3
  249. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  250. package/dist/modules/workflow/git/git.service.js +325 -60
  251. package/dist/modules/workflow/git/git.service.js.map +1 -1
  252. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  253. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  254. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  255. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  256. package/dist/modules/workflow/workflow.routes.d.ts +2 -1
  257. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  258. package/dist/modules/workflow/workflow.routes.js +98 -9
  259. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +133 -9
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +462 -52
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  265. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  266. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  267. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  268. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  269. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  270. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  271. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  272. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  273. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  274. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspace.routes.js +11 -14
  276. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  277. package/dist/modules/workspace/workspace.service.d.ts +38 -1
  278. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  279. package/dist/modules/workspace/workspace.service.js +131 -20
  280. package/dist/modules/workspace/workspace.service.js.map +1 -1
  281. package/dist/shared/domain-errors.d.ts +20 -0
  282. package/dist/shared/domain-errors.d.ts.map +1 -1
  283. package/dist/shared/domain-errors.js +29 -0
  284. package/dist/shared/domain-errors.js.map +1 -1
  285. package/dist/shared/fs-errors.d.ts +8 -0
  286. package/dist/shared/fs-errors.d.ts.map +1 -0
  287. package/dist/shared/fs-errors.js +11 -0
  288. package/dist/shared/fs-errors.js.map +1 -0
  289. package/dist/shared/fs-walk.d.ts +20 -6
  290. package/dist/shared/fs-walk.d.ts.map +1 -1
  291. package/dist/shared/fs-walk.js +35 -27
  292. package/dist/shared/fs-walk.js.map +1 -1
  293. package/dist/shared/kb-walk.d.ts +52 -0
  294. package/dist/shared/kb-walk.d.ts.map +1 -0
  295. package/dist/shared/kb-walk.js +76 -0
  296. package/dist/shared/kb-walk.js.map +1 -0
  297. package/dist/shared/path-order.d.ts +18 -0
  298. package/dist/shared/path-order.d.ts.map +1 -0
  299. package/dist/shared/path-order.js +35 -0
  300. package/dist/shared/path-order.js.map +1 -0
  301. package/dist/shared/token-crypto.d.ts +17 -2
  302. package/dist/shared/token-crypto.d.ts.map +1 -1
  303. package/dist/shared/token-crypto.js +25 -8
  304. package/dist/shared/token-crypto.js.map +1 -1
  305. package/dist/shared/ttl-cache.d.ts +28 -1
  306. package/dist/shared/ttl-cache.d.ts.map +1 -1
  307. package/dist/shared/ttl-cache.js +33 -1
  308. package/dist/shared/ttl-cache.js.map +1 -1
  309. package/kb-template/.bevelignore +1 -1
  310. package/kb-template/AGENTS.md +95 -30
  311. package/kb-template/Skills/.gitkeep +0 -0
  312. package/migrations/0005_github_facade.sql +25 -0
  313. package/migrations/meta/0005_snapshot.json +1717 -0
  314. package/migrations/meta/_journal.json +7 -0
  315. package/package.json +6 -6
  316. package/src/__tests__/core-config.admin.test.ts +21 -0
  317. package/src/__tests__/kb-layout-config.test.ts +110 -0
  318. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  319. package/src/core/catalog-cache-invalidation.ts +92 -0
  320. package/src/core/create-core-server.ts +126 -7
  321. package/src/core/create-core-services.ts +173 -41
  322. package/src/core-config.ts +14 -7
  323. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  324. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  325. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  326. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  327. package/src/modules/access/access-control.interface.ts +20 -9
  328. package/src/modules/access/access-control.service.ts +240 -78
  329. package/src/modules/access/access.routes.ts +96 -14
  330. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  331. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  332. package/src/modules/access-model/access-grammar.ts +114 -2
  333. package/src/modules/access-model/group-files.ts +4 -0
  334. package/src/modules/access-model/kb-read-filter.ts +22 -0
  335. package/src/modules/access-model/plugin-principals.ts +137 -0
  336. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
  337. package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
  338. package/src/modules/connection-probe/connection-probe.service.ts +734 -0
  339. package/src/modules/connection-probe/index.ts +2 -0
  340. package/src/modules/database/core-schema.ts +54 -0
  341. package/src/modules/diff/diff.routes.ts +9 -5
  342. package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
  343. package/src/modules/kb-fs/mutex.ts +50 -5
  344. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  345. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  346. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  347. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  348. package/src/modules/kb-sync/index.ts +19 -0
  349. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  350. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  351. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  352. package/src/modules/kb-sync/sync-auth.ts +118 -0
  353. package/src/modules/kb-sync/sync-payload.ts +143 -0
  354. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  355. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  356. package/src/modules/marketplace/git-http.routes.ts +193 -0
  357. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  358. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  359. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  360. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  361. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  362. package/src/modules/marketplace/github-facade/index.ts +25 -0
  363. package/src/modules/marketplace/index.ts +3 -0
  364. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  365. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  366. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  367. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  368. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  369. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  370. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  371. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  372. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  373. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  374. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  375. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  376. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  377. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  378. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  379. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  380. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  381. package/src/modules/plugins/discovery/native.source.ts +122 -0
  382. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  383. package/src/modules/plugins/index.ts +10 -0
  384. package/src/modules/plugins/plugin-links.service.ts +363 -0
  385. package/src/modules/plugins/plugin-links.ts +184 -0
  386. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  387. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  388. package/src/modules/plugins/plugins.contract.ts +28 -1
  389. package/src/modules/plugins/plugins.routes.ts +128 -11
  390. package/src/modules/plugins/plugins.service.ts +106 -53
  391. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
  392. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
  393. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
  394. package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
  395. package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
  396. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  397. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  398. package/src/modules/settings/deployment-settings.service.ts +95 -2
  399. package/src/modules/settings/setup.routes.ts +24 -0
  400. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  401. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  402. package/src/modules/skills/index.ts +1 -0
  403. package/src/modules/skills/pending-skills.service.ts +7 -5
  404. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  405. package/src/modules/skills/skills.contract.ts +32 -0
  406. package/src/modules/skills/skills.routes.ts +59 -3
  407. package/src/modules/skills/skills.service.ts +84 -22
  408. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  409. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  410. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
  411. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  412. package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
  413. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
  414. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
  415. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
  416. package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
  417. package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
  418. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  419. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
  420. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
  421. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  422. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
  423. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
  424. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  425. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  426. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
  427. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  428. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  429. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  430. package/src/modules/workflow/git/git.service.ts +354 -63
  431. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  432. package/src/modules/workflow/workflow.routes.ts +104 -4
  433. package/src/modules/workflow/workflow.service.ts +505 -64
  434. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  435. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  436. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  437. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  438. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  439. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  440. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  441. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  442. package/src/modules/workspace/workspace.routes.ts +17 -21
  443. package/src/modules/workspace/workspace.service.ts +128 -19
  444. package/src/shared/__tests__/filename.test.ts +0 -0
  445. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  446. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  447. package/src/shared/__tests__/path-order.test.ts +24 -0
  448. package/src/shared/__tests__/token-crypto.test.ts +34 -0
  449. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  450. package/src/shared/domain-errors.ts +29 -0
  451. package/src/shared/fs-errors.ts +10 -0
  452. package/src/shared/fs-walk.ts +38 -23
  453. package/src/shared/kb-walk.ts +95 -0
  454. package/src/shared/path-order.ts +33 -0
  455. package/src/shared/token-crypto.ts +28 -10
  456. package/src/shared/ttl-cache.ts +33 -1
@@ -1,8 +1,31 @@
1
1
  import { describe, expect, it, vi, afterEach } from 'vitest';
2
- import { descriptorsFromMcpJson } from '../mcp-json-discovery.js';
2
+ import { descriptorsFromMcpJson, judgeMcpServerEntry } from '../mcp-json-discovery.js';
3
3
 
4
4
  afterEach(() => vi.restoreAllMocks());
5
5
 
6
+ describe('judgeMcpServerEntry', () => {
7
+ it('keeps every accepted header and env key — one spelled __proto__ included', () => {
8
+ // Parsed from text, as a repository file is: JSON.parse makes `__proto__`
9
+ // an own key, and a plain-object copy would turn it into a prototype
10
+ // assignment and lose it.
11
+ const http = judgeMcpServerEntry(
12
+ 'vendor',
13
+ JSON.parse('{"type":"streamable-http","url":"https://x.example","headers":{"__proto__":"p","X-A":"a"}}'),
14
+ );
15
+ expect(http.ok).toBe(true);
16
+ if (!http.ok || http.transport !== 'streamable-http') throw new Error('unreachable');
17
+ expect(Object.keys(http.entry.headers ?? {}).sort()).toEqual(['X-A', '__proto__']);
18
+ expect(http.entry.headers?.['__proto__']).toBe('p');
19
+
20
+ const stdio = judgeMcpServerEntry(
21
+ 'launch',
22
+ JSON.parse('{"type":"stdio","command":"run","env":{"__proto__":"p"}}'),
23
+ );
24
+ if (!stdio.ok || stdio.transport !== 'stdio') throw new Error('unreachable');
25
+ expect(stdio.entry.env?.['__proto__']).toBe('p');
26
+ });
27
+ });
28
+
6
29
  const MANIFEST = JSON.stringify({
7
30
  name: 'gtm',
8
31
  extensions: {
@@ -37,6 +60,17 @@ describe('descriptorsFromMcpJson', () => {
37
60
  ]);
38
61
  });
39
62
 
63
+ it('refuses an sse server rather than rebuilding it as a transport it is not', () => {
64
+ // The pinned MCP client has no sse transport; emitting `http` for an sse
65
+ // server configures a handshake the server does not speak.
66
+ const out = descriptorsFromMcpJson(
67
+ 'GTM',
68
+ JSON.stringify({ mcpServers: { legacy: { type: 'sse', url: 'https://mcp.legacy.example/sse' } } }),
69
+ null,
70
+ );
71
+ expect(out).toEqual([]);
72
+ });
73
+
40
74
  it('merges extension auth over mcp.json literals and carries variables + description', () => {
41
75
  const out = descriptorsFromMcpJson(
42
76
  'GTM',
@@ -192,6 +192,8 @@ describe('manual namespaces are unique, not merely manual names', () => {
192
192
  */
193
193
  async function writeMcpServer(folder: string, serverName: string): Promise<void> {
194
194
  await mkdir(join(pluginsDir(), folder), { recursive: true });
195
+ // A plugin is a folder with a manifest; the servers ride in its mcp.json.
196
+ await writeFile(join(pluginsDir(), folder, 'plugin.json'), `{"name":"${folder.toLowerCase()}"}`);
195
197
  await writeFile(
196
198
  join(pluginsDir(), folder, 'mcp.json'),
197
199
  JSON.stringify({ mcpServers: { [serverName]: { type: 'streamable-http', url: 'https://v.example/mcp' } } }),
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizeToolManual } from '../tool-manuals.service.js';
3
+
4
+ /**
5
+ * The `healthCheck:` block is a URL this SERVER will fetch with the caller's
6
+ * credential attached, unattended, on every save and re-check. That makes it a
7
+ * second fetch target on the same footing as the manual's own `url`, so it is
8
+ * policed by the same SSRF rule — and held to one extra rule of its own: it may
9
+ * not mutate.
10
+ */
11
+
12
+ const toolFile = (body: string) => `---
13
+ name: acme
14
+ type: http
15
+ url: https://api.acme.test/utcp
16
+ headers:
17
+ Authorization: Bearer \${API_KEY}
18
+ ${body}---
19
+ notes
20
+ `;
21
+
22
+ const parse = (body: string) => normalizeToolManual('acme', 'Plugins/acme.tool', toolFile(body));
23
+
24
+ describe('`.tool` healthCheck', () => {
25
+ it('is absent when nothing is declared — the tool is simply unverifiable', () => {
26
+ expect(parse('').healthCheck).toBeUndefined();
27
+ });
28
+
29
+ it('inherits the manual\'s headers, so a one-line declaration still authenticates', () => {
30
+ // This is what makes the common case cheap: the credential already lives in
31
+ // `headers`, and re-typing it on the probe would be a chance to get it wrong.
32
+ expect(parse('healthCheck:\n url: https://api.acme.test/me\n').healthCheck).toEqual({
33
+ url: 'https://api.acme.test/me',
34
+ headers: { Authorization: 'Bearer ${API_KEY}' },
35
+ });
36
+ });
37
+
38
+ it('keeps its own headers when it declares them', () => {
39
+ const hc = parse('healthCheck:\n url: https://api.acme.test/me\n headers:\n X-Key: ${API_KEY}\n').healthCheck;
40
+ expect(hc?.headers).toEqual({ 'X-Key': '${API_KEY}' });
41
+ });
42
+
43
+ it("an INLINE manual's probe inherits NO top-level headers — execution never sends them", () => {
44
+ // An inline manual's real calls go through each embedded tool's own call
45
+ // template; nothing ever sends the manual's top-level `headers:`. A probe
46
+ // that inherited them proved a request no call makes — `Connected` about
47
+ // the wrong request. An inline probe declares its headers on the
48
+ // healthCheck itself, or sends none.
49
+ const inline = normalizeToolManual(
50
+ 'acme',
51
+ 'Plugins/acme.tool',
52
+ `---
53
+ name: acme
54
+ type: inline
55
+ headers:
56
+ Authorization: Bearer \${API_KEY}
57
+ healthCheck:
58
+ url: https://api.acme.test/me
59
+ tools: []
60
+ ---
61
+ `,
62
+ );
63
+ expect(inline.healthCheck?.headers).toBeUndefined();
64
+ });
65
+
66
+ it('refuses a method that could mutate', () => {
67
+ // Silently downgrading POST to GET would leave the author believing they had
68
+ // declared something we are not doing.
69
+ expect(() => parse('healthCheck:\n url: https://api.acme.test/me\n method: POST\n')).toThrow(
70
+ /may not mutate/,
71
+ );
72
+ });
73
+
74
+ it('refuses a probe pointed at an internal host', () => {
75
+ expect(() => parse('healthCheck:\n url: http://169.254.169.254/latest/meta-data\n')).toThrow(
76
+ /not allowed/,
77
+ );
78
+ });
79
+
80
+ it('refuses a declaration with no url', () => {
81
+ expect(() => parse('healthCheck:\n method: GET\n')).toThrow(/must have a `url`/);
82
+ });
83
+
84
+ /**
85
+ * A local-only `.tool` is never fetched by this server, so its probe is exempt
86
+ * from the guard for exactly the same reason its `url` is.
87
+ */
88
+ it('exempts a local-only tool, as it does for the manual url', () => {
89
+ const local = normalizeToolManual(
90
+ 'acme',
91
+ 'Plugins/acme.tool',
92
+ `---
93
+ name: acme
94
+ type: http
95
+ remote: false
96
+ url: http://localhost:9000/utcp
97
+ healthCheck:
98
+ url: http://localhost:9000/me
99
+ ---
100
+ `,
101
+ );
102
+ expect(local.healthCheck?.url).toBe('http://localhost:9000/me');
103
+ });
104
+ });
@@ -82,6 +82,62 @@ describe('ToolManualService', () => {
82
82
  expect(list.find((m) => m.name === 'billing')!.type).toBe('http');
83
83
  });
84
84
 
85
+ test('surfaces a variable referenced ONLY by a health check, and keeps the probe off the summary', async () => {
86
+ // Two halves of the same contract. The probe's `${VAR}` has to reach the
87
+ // secrets UI or nobody can ever fill it in and the tool reports
88
+ // `unverifiable` forever — but the probe's HEADERS must not ride the
89
+ // browser-facing summary, since a `.tool` may write a literal token there.
90
+ const tools = join(root, wsId, KB_DIR, 'Plugins');
91
+ await writeFile(
92
+ join(tools, 'probe.tool'),
93
+ JSON.stringify({
94
+ name: 'probe',
95
+ type: 'http',
96
+ url: 'https://api.example.com/utcp',
97
+ healthCheck: { url: 'https://api.example.com/me', headers: { 'X-Key': '${PROBE_ONLY_KEY}' } },
98
+ }),
99
+ );
100
+
101
+ const summary = (await svc().listAccessible('user@x.eu')).find((m) => m.name === 'probe')!;
102
+ expect(summary.variables?.map((v) => v.name)).toContain('PROBE_ONLY_KEY');
103
+ expect(summary).not.toHaveProperty('healthCheck');
104
+
105
+ // The server still reaches it, through the accessor that never serializes.
106
+ const target = await svc().probeTargetFor('user@x.eu', 'probe');
107
+ expect(target?.healthCheck?.headers).toEqual({ 'X-Key': '${PROBE_ONLY_KEY}' });
108
+ });
109
+
110
+ test('builds a call template only for the probe that actually dials one', async () => {
111
+ // The template is dialled by exactly one probe: an `mcp` manual, reachable
112
+ // from this process, that declared no health check. Building it for the
113
+ // others costs a validation pass per probe and warn-logs about a value
114
+ // nothing was going to use.
115
+ const tools = join(root, wsId, KB_DIR, 'Plugins');
116
+ await writeFile(
117
+ join(tools, 'chat.tool'),
118
+ JSON.stringify({ name: 'chat', type: 'mcp', url: 'https://mcp.example.com' }),
119
+ );
120
+ await writeFile(
121
+ join(tools, 'checked.tool'),
122
+ JSON.stringify({
123
+ name: 'checked',
124
+ type: 'mcp',
125
+ url: 'https://mcp.example.com',
126
+ healthCheck: { url: 'https://api.example.com/me' },
127
+ }),
128
+ );
129
+
130
+ const service = svc();
131
+ // Asserted through the template's own type, not `not.toBeNull()`: the
132
+ // optional chain yields `undefined` when the lookup itself fails, and
133
+ // `undefined` is not null — so the weaker form passes on no target at all.
134
+ expect((await service.probeTargetFor('user@x.eu', 'chat'))?.callTemplate?.call_template_type).toBe('mcp');
135
+ // A declared health check wins for every type, so this one never dials it.
136
+ expect((await service.probeTargetFor('user@x.eu', 'checked'))?.callTemplate).toBeNull();
137
+ // An http manual is probed only by what it declares, never by a handshake.
138
+ expect((await service.probeTargetFor('user@x.eu', 'billing'))?.callTemplate).toBeNull();
139
+ });
140
+
85
141
  test('ACL filters out manuals the user cannot read', async () => {
86
142
  const list = await svc(denyBilling).listAccessible('user@x.eu');
87
143
  expect(list.map((m) => m.name)).toEqual(['weather']);
@@ -195,6 +195,121 @@ export function validatedVariables(raw: unknown): ToolVariable[] | null {
195
195
  * materialization is a later phase — so a stdio server relying on them will
196
196
  * fail to spawn until then; bare-command servers (`npx …`) work today.
197
197
  */
198
+ /**
199
+ * The ONE judgement of an `mcp.json` server entry — its name and its
200
+ * transport — shared by the manual scanner (which layers the platform's
201
+ * extension data and its reachability gate on top), the bundle dialect's
202
+ * registry (whose configs become entries) and the marketplace compiler
203
+ * (which ships only what a client could run). An entry that fails here fails
204
+ * everywhere, for one reason; an entry that passes is returned NORMALISED to
205
+ * the portable shape every consumer emits.
206
+ */
207
+ export type McpEntryVerdict =
208
+ | { ok: true; transport: 'stdio'; entry: PortableStdioEntry }
209
+ | { ok: true; transport: 'streamable-http'; entry: PortableHttpEntry }
210
+ | { ok: false; reason: string };
211
+
212
+ /** A stdio server as a client launches it — every field already the type the launcher needs. */
213
+ export interface PortableStdioEntry {
214
+ type: 'stdio';
215
+ command: string;
216
+ args: string[];
217
+ env?: Record<string, string>;
218
+ cwd?: string;
219
+ }
220
+ /** An http server as a client dials it — an endpoint, never a credential. */
221
+ export interface PortableHttpEntry {
222
+ type: 'streamable-http';
223
+ url: string;
224
+ headers?: Record<string, string>;
225
+ }
226
+
227
+ /**
228
+ * A record whose every value is a string, or null — no coercion, no cast.
229
+ * Built without a prototype: the keys are HTTP header and environment
230
+ * variable names, and a plain object would silently swallow one spelled
231
+ * `__proto__` (a prototype assignment, not a property) — every accepted key
232
+ * must come out the other side.
233
+ */
234
+ function stringMap(v: unknown): Record<string, string> | null {
235
+ if (!isRecord(v)) return null;
236
+ const out: Record<string, string> = Object.create(null) as Record<string, string>;
237
+ for (const [k, val] of Object.entries(v)) {
238
+ if (typeof val !== 'string') return null;
239
+ out[k] = val;
240
+ }
241
+ return out;
242
+ }
243
+
244
+ export function judgeMcpServerEntry(name: string, raw: unknown): McpEntryVerdict {
245
+ // The name rule (lowercase, alphanumeric first) also keeps every object
246
+ // prototype key — `__proto__`, `constructor` — out of the plain-object
247
+ // maps consumers build from these names.
248
+ if (!SERVER_NAME_RE.test(name)) {
249
+ return {
250
+ ok: false,
251
+ reason: 'the name is the secret namespace and route slug, so it must be lowercase alphanumeric with `_`/`-`',
252
+ };
253
+ }
254
+ if (!isRecord(raw) || typeof raw.type !== 'string') return { ok: false, reason: 'no transport type' };
255
+ if (raw.type === 'stdio') {
256
+ if (typeof raw.command !== 'string' || raw.command.length === 0) return { ok: false, reason: 'no command' };
257
+ const entry: PortableStdioEntry = { type: 'stdio', command: raw.command, args: [] };
258
+ if (raw.args !== undefined) {
259
+ // A launcher gets the strings it was given, never a stringified object.
260
+ if (!Array.isArray(raw.args) || !raw.args.every((a) => typeof a === 'string')) {
261
+ return { ok: false, reason: 'args must be a list of strings' };
262
+ }
263
+ entry.args = raw.args;
264
+ }
265
+ if (raw.env !== undefined) {
266
+ const env = stringMap(raw.env);
267
+ if (env === null) return { ok: false, reason: 'env must be a map of strings' };
268
+ entry.env = env;
269
+ }
270
+ if (raw.cwd !== undefined) {
271
+ if (typeof raw.cwd !== 'string') return { ok: false, reason: 'cwd must be a string' };
272
+ entry.cwd = raw.cwd;
273
+ }
274
+ return { ok: true, transport: 'stdio', entry };
275
+ }
276
+ if (raw.type === 'sse') {
277
+ // The pinned `@utcp/mcp` speaks `stdio` and streamable `http` — there
278
+ // is no sse transport in its schema, so a declaration claiming one either
279
+ // fails validation or, worse, dials a handshake the server does not
280
+ // speak. Refusing names the fix; silently rebuilding as http used to
281
+ // configure exactly that wrong handshake.
282
+ return {
283
+ ok: false,
284
+ reason: 'the MCP client has no `sse` transport — declare the server as `streamable-http` if it supports it',
285
+ };
286
+ }
287
+ if (raw.type === 'streamable-http') {
288
+ if (typeof raw.url !== 'string' || raw.url.length === 0) return { ok: false, reason: 'no url' };
289
+ let parsed: URL;
290
+ try {
291
+ parsed = new URL(raw.url);
292
+ } catch {
293
+ return { ok: false, reason: 'url must be http(s)' };
294
+ }
295
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return { ok: false, reason: 'url must be http(s)' };
296
+ // A portable entry is an endpoint. A credential belongs in the vault and
297
+ // reaches a client through the platform, never verbatim in a config that
298
+ // is compiled into a marketplace and cloned onto every member's machine.
299
+ if (parsed.username || parsed.password) return { ok: false, reason: 'url must not carry credentials' };
300
+ const entry: PortableHttpEntry = { type: 'streamable-http', url: raw.url };
301
+ if (raw.headers !== undefined) {
302
+ const headers = stringMap(raw.headers);
303
+ if (headers === null) return { ok: false, reason: 'headers must be a map of strings' };
304
+ entry.headers = headers;
305
+ }
306
+ return { ok: true, transport: 'streamable-http', entry };
307
+ }
308
+ // Unknown transport: the spec says an unknown `type` invalidates the
309
+ // ENTRY, not the file — the caller skips it and keeps its siblings.
310
+ return { ok: false, reason: `unknown type "${raw.type}"` };
311
+ }
312
+
198
313
  export function descriptorsFromMcpJson(
199
314
  pluginFolder: string,
200
315
  mcpJsonText: string,
@@ -227,18 +342,14 @@ export function descriptorsFromMcpJson(
227
342
  const mcpJsonPath = `${PLUGINS_DIR}/${pluginFolder}/${PLUGIN_MCP_FILE}`;
228
343
 
229
344
  const out: ToolManualDescriptor[] = [];
230
- for (const [name, raw] of Object.entries(mcp.mcpServers)) {
231
- if (!SERVER_NAME_RE.test(name)) {
232
- console.warn(
233
- `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: the name is the secret ` +
234
- 'namespace and route slug, so it must be lowercase alphanumeric with `_`/`-`.',
235
- );
236
- continue;
237
- }
238
- if (!isRecord(raw) || typeof raw.type !== 'string') {
239
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: no transport type.`);
345
+ for (const [name, rawEntry] of Object.entries(mcp.mcpServers)) {
346
+ const verdict = judgeMcpServerEntry(name, rawEntry);
347
+ if (!verdict.ok) {
348
+ console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ${verdict.reason}.`);
240
349
  continue;
241
350
  }
351
+ // Narrowed by the verdict: an entry that passed is a record.
352
+ const raw = rawEntry as Record<string, unknown>;
242
353
  // The extension entry is knowledge-base content too — same zero-trust
243
354
  // parse as the rest: a non-object entry reads as "no extension data".
244
355
  const ext: HexisMcpServerExtension = isRecord(extensions[name])
@@ -274,79 +385,46 @@ export function descriptorsFromMcpJson(
274
385
  ...(variables.length > 0 ? { variables } : {}),
275
386
  };
276
387
 
277
- if (raw.type === 'stdio') {
278
- if (typeof raw.command !== 'string' || raw.command.length === 0) {
279
- console.warn(`[tool-manuals] skipping stdio server "${name}" in ${mcpJsonPath}: no command.`);
280
- continue;
281
- }
282
- out.push({
283
- ...shared,
284
- remote: false,
285
- stdio: {
286
- command: raw.command,
287
- args: Array.isArray(raw.args) ? raw.args.map(String) : [],
288
- env: isRecord(raw.env) ? (raw.env as Record<string, string>) : undefined,
289
- cwd: typeof raw.cwd === 'string' ? raw.cwd : undefined,
290
- },
291
- });
388
+ if (verdict.transport === 'stdio') {
389
+ const { command, args, env, cwd } = verdict.entry;
390
+ out.push({ ...shared, remote: false, stdio: { command, args, env, cwd } });
292
391
  continue;
293
392
  }
294
393
 
295
- if (raw.type === 'streamable-http' || raw.type === 'sse') {
296
- if (typeof raw.url !== 'string' || raw.url.length === 0) {
297
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: no url.`);
298
- continue;
299
- }
300
- // Remote-capable servers get the same SSRF gate `.tool` urls pass —
301
- // otherwise mcp.json becomes the way to point the backend at loopback,
302
- // private ranges, or the cloud metadata endpoint. A `local: true` entry
303
- // is exempt because loopback is exactly what local MEANS, and only the
304
- // user's own machine ever dials it.
305
- // Scheme first, for EVERYONE: `local: true` exempts a server from the
306
- // private-network reachability policy, not from being http(s) at all.
307
- let schemeOk = false;
394
+ const url = verdict.entry.url;
395
+ // Remote-capable servers get the same SSRF gate `.tool` urls pass —
396
+ // otherwise mcp.json becomes the way to point the backend at loopback,
397
+ // private ranges, or the cloud metadata endpoint. A `local: true` entry
398
+ // is exempt because loopback is exactly what local MEANS, and only the
399
+ // user's own machine ever dials it. (The scheme itself was judged for
400
+ // everyone above: `local: true` exempts a server from the reachability
401
+ // policy, not from being http(s) at all.)
402
+ if (ext.local !== true) {
308
403
  try {
309
- const u = new URL(raw.url);
310
- schemeOk = u.protocol === 'http:' || u.protocol === 'https:';
311
- } catch {
312
- schemeOk = false;
313
- }
314
- if (!schemeOk) {
315
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: url must be http(s).`);
404
+ assertSafeFetchUrl(url, { label: `mcp server "${name}" url` });
405
+ } catch (err) {
406
+ console.warn(
407
+ `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ` +
408
+ `${err instanceof Error ? err.message : String(err)} (declare it \`local: true\` if it is deliberately private).`,
409
+ );
316
410
  continue;
317
411
  }
318
- if (ext.local !== true) {
319
- try {
320
- assertSafeFetchUrl(raw.url, { label: `mcp server "${name}" url` });
321
- } catch (err) {
322
- console.warn(
323
- `[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ` +
324
- `${err instanceof Error ? err.message : String(err)} (declare it \`local: true\` if it is deliberately private).`,
325
- );
326
- continue;
327
- }
328
- }
329
- // Extension headers (auth, `${VAR}` refs) win over mcp.json's literal
330
- // ones on a key collision: the portable file cannot carry a credential,
331
- // so when both name the same header the extension is the operative one.
332
- // Both sides pass the isRecord gate — spreading a malformed non-object
333
- // value (a string, say) would scatter its indices into header keys.
334
- const headers = {
335
- ...(isRecord(raw.headers) ? (raw.headers as Record<string, string>) : {}),
336
- ...(isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {}),
337
- };
338
- out.push({
339
- ...shared,
340
- url: raw.url,
341
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
342
- ...(ext.local === true ? { remote: false } : {}),
343
- });
344
- continue;
345
412
  }
346
-
347
- // Unknown transport: the spec says an unknown `type` invalidates the
348
- // ENTRY, not the file — skip it, keep its siblings.
349
- console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: unknown type "${raw.type}".`);
413
+ // Extension headers (auth, `${VAR}` refs) win over mcp.json's literal
414
+ // ones on a key collision: the portable file cannot carry a credential,
415
+ // so when both name the same header the extension is the operative one.
416
+ // Both sides pass the isRecord gate — spreading a malformed non-object
417
+ // value (a string, say) would scatter its indices into header keys.
418
+ const headers = {
419
+ ...(verdict.entry.headers ?? {}),
420
+ ...(isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {}),
421
+ };
422
+ out.push({
423
+ ...shared,
424
+ url,
425
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
426
+ ...(ext.local === true ? { remote: false } : {}),
427
+ });
350
428
  }
351
429
  return out;
352
430
  }
@@ -174,10 +174,19 @@ export class McpServerEditService {
174
174
  const servers = mcp.mcpServers as Record<string, unknown>;
175
175
  if (!(name in servers)) throw new McpServerEditError('No such server.', 404);
176
176
 
177
- if (write.transport !== 'streamable-http' && write.transport !== 'sse' && write.transport !== 'stdio') {
178
- // Persisting an unknown transport would save an entry discovery then
179
- // refuses — an unusable server with no error at the moment it was made.
180
- throw new McpServerEditError(`Unknown transport "${String(write.transport)}".`, 422);
177
+ if (write.transport !== 'streamable-http' && write.transport !== 'stdio') {
178
+ // Persisting a transport discovery refuses would save an entry that
179
+ // vanishes from the catalog the moment it is written — an unusable
180
+ // server with no error at the moment it was made. `sse` is refused BY
181
+ // NAME with the fix spelled out, because the pinned MCP client has no
182
+ // sse transport; an EXISTING sse entry stays readable here, so it can
183
+ // be edited to `streamable-http` rather than being stranded.
184
+ throw new McpServerEditError(
185
+ write.transport === 'sse'
186
+ ? 'The MCP client has no `sse` transport — declare the server as `streamable-http` if it supports it.'
187
+ : `Unknown transport "${String(write.transport)}".`,
188
+ 422,
189
+ );
181
190
  }
182
191
  // The EFFECTIVE value of every field, PATCH-over-stored (see
183
192
  // McpServerWrite): `undefined` keeps what the files already say, a
@@ -88,6 +88,57 @@ export interface ToolVariable {
88
88
  oauth?: ToolVariableOAuth;
89
89
  }
90
90
 
91
+ /**
92
+ * An optional cheap, read-only call that proves a credential actually WORKS.
93
+ *
94
+ * A `type: mcp` manual needs none — its handshake is authenticated, so merely
95
+ * connecting already tests the token. `http` and `inline` manuals have no such
96
+ * moment: registering an `http` manual fetches its DESCRIPTION, which providers
97
+ * usually serve publicly, so a wrong key sails through; an `inline` manual's
98
+ * discovery template points at Bevel's own API and never touches the provider
99
+ * at all. Without a declaration there is genuinely nothing to call, and the
100
+ * platform reports the connection as unverified rather than guessing.
101
+ *
102
+ * NOT inferred from the manual's own tools. Picking one automatically would
103
+ * mean calling an arbitrary tool with invented arguments — a probe that could
104
+ * send a message or create a record is worse than no probe. The author names a
105
+ * safe endpoint or the tool stays unverified.
106
+ *
107
+ * `${VAR}` refs resolve from the caller's vault exactly as they do in `url` and
108
+ * `headers`, which is the whole point: the probe must carry the same credential
109
+ * the real calls carry. Any 2xx is a pass; 401/403 is a rejection; anything
110
+ * else is treated as the provider being unwell rather than the key being wrong.
111
+ */
112
+ export interface ToolHealthCheck {
113
+ /** Absolute URL to call. May contain `${VAR}` refs; SSRF-checked like `url`. */
114
+ url: string;
115
+ /** Default `GET`. A health check may not mutate, so nothing else is allowed. */
116
+ method?: 'GET';
117
+ /** Defaults to the manual's own `headers` — usually where the credential is. */
118
+ headers?: Record<string, string>;
119
+ }
120
+
121
+ /**
122
+ * One manual, reduced to exactly what testing its credential requires.
123
+ *
124
+ * Server-internal, like {@link ToolHealthCheck} and for the same reason: both
125
+ * `healthCheck.headers` and `callTemplate` can carry a literal token.
126
+ */
127
+ export interface ToolProbeTarget {
128
+ name: string;
129
+ type: ToolManualType;
130
+ /** `false` for a local-only tool this server is not the one that can reach it. */
131
+ remote?: boolean;
132
+ /** The probe the `.tool` declares, if any. */
133
+ healthCheck?: ToolHealthCheck;
134
+ /**
135
+ * The validated UTCP call template, or `null` when the manual doesn't produce
136
+ * a valid one. Only the `mcp` probe uses it — its handshake IS the test — but
137
+ * it is built here so the probe never has to walk the catalog again.
138
+ */
139
+ callTemplate: CallTemplate | null;
140
+ }
141
+
91
142
  /**
92
143
  * For a `type: mcp` tool, what an admin must do to make the remote server
93
144
  * reachable — derived from OAuth auto-discovery, which a non-mcp tool has no
@@ -140,6 +191,15 @@ export interface ToolManualDescriptorBase {
140
191
  variables?: ToolVariable[];
141
192
  /** For `type: mcp`: the admin-facing setup requirement from auto-discovery. */
142
193
  setup?: ToolManualSetup;
194
+ /**
195
+ * Optional credential probe — see {@link ToolHealthCheck}. Declared on
196
+ * `http`/`inline` manuals, and honoured on `type: mcp` too, where it
197
+ * OVERRIDES the default handshake probe (a server whose health endpoint is
198
+ * cheaper or more truthful than a full MCP handshake says so here).
199
+ * INTERNAL: lives on the descriptor, never on {@link ToolManualSummary},
200
+ * because it carries `headers`.
201
+ */
202
+ healthCheck?: ToolHealthCheck;
143
203
  }
144
204
 
145
205
  /** The spawn spec of a stdio-declared MCP server (a plugin `mcp.json` entry). */
@@ -197,6 +257,10 @@ export interface ToolManualSummary {
197
257
  remote?: boolean;
198
258
  /** For `type: mcp`: the admin-facing setup requirement from auto-discovery. */
199
259
  setup?: ToolManualSetup;
260
+ // NOTE: no `healthCheck` here, deliberately. This type is serialized straight
261
+ // to the browser by the tool endpoints, and a probe carries `headers` — which
262
+ // a `.tool` author may write as a literal token rather than a `${VAR}` ref.
263
+ // The server reads probe config through `IToolManualService.probeTargetFor`.
200
264
  }
201
265
 
202
266
  /** One thing an `inline` manual's embedded tool list says the assistant can do. */
@@ -269,6 +333,31 @@ export interface IToolManualService {
269
333
  * a `resolve` reads the shared row or the caller's row.
270
334
  */
271
335
  scopeOfVariable(effectiveKey: string): Promise<ToolVariableScope>;
336
+ /**
337
+ * Everything the credential probe needs about one readable manual, resolved
338
+ * in a SINGLE catalog + ACL pass.
339
+ *
340
+ * One accessor rather than three because the probe needs three facts that all
341
+ * come from the same file — is it local-only, does it declare a health check,
342
+ * and what call template would reach it — and asking for them separately made
343
+ * one probe walk the catalog four times, building call templates for every
344
+ * manual in the workspace to use exactly one of them.
345
+ *
346
+ * Deliberately NOT reachable through {@link ToolManualSummary}: that type is
347
+ * serialized straight to the browser by the tool endpoints, and a probe
348
+ * carries `headers` — which a `.tool` author may write as a literal token
349
+ * rather than a `${VAR}` ref. Probe config is internal to the server, so it
350
+ * travels by its own accessor and never rides a public DTO.
351
+ *
352
+ * Addressed by SLUG, which is what the route has: resolving the slug here
353
+ * rather than making the caller map it to a name first is what reduces a
354
+ * probe to one pass. `ToolProbeTarget.name` carries the UTCP namespace back
355
+ * out, since that is what the probe's `${VAR}` lookups are keyed by.
356
+ *
357
+ * `null` when no such manual exists OR the caller can't read it — the two are
358
+ * indistinguishable on purpose, as everywhere else in this contract.
359
+ */
360
+ probeTargetFor(userEmail: string, slug: string): Promise<ToolProbeTarget | null>;
272
361
  /**
273
362
  * The per-user (`user`-scoped) variables a manual declares, each with the vault
274
363
  * key (`<manual>_<VAR>`) the caller's value is stored under, its bare name, and