@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
@@ -13,6 +13,8 @@ import { WorkflowDomainError } from '../../shared/domain-errors.js';
13
13
  import type { WorkspaceService } from '../workspace/workspace.service.js';
14
14
  import { pluginsWorkspaceId } from './plugins.service.js';
15
15
  import { PluginProvisionError, type PluginProvisionService } from './plugin-provision.service.js';
16
+ import { PluginLinkError, type PluginLinksService } from './plugin-links.service.js';
17
+ import { PluginRenameError, type PluginRenameService } from './plugin-rename.service.js';
16
18
  import type { JoinRequestsService } from './join-requests.service.js';
17
19
  import type {
18
20
  PluginCatalogEntry,
@@ -61,13 +63,121 @@ export function createPluginsRoutes(
61
63
  provision: PluginProvisionService,
62
64
  kbDirName: string,
63
65
  resolveUser: (req: express.Request) => Promise<AuthUser | null>,
66
+ /** Optional: a host without the link machinery simply has no link routes. */
67
+ links?: PluginLinksService,
68
+ /** Optional: a host without it has no rename route. */
69
+ rename?: PluginRenameService,
64
70
  ): express.Router {
65
71
  const router = express.Router();
66
72
 
73
+ /**
74
+ * Rename a plugin — its identifier, its display name, or both. The
75
+ * MANAGER's verb (write on the folder's access.md, the gate linking uses).
76
+ * An identifier change rewrites every grant naming the old identifier in
77
+ * the same commit; see `PluginRenameService`.
78
+ *
79
+ * PATCH /api/plugins/:name { name?, displayName? } → { name, displayName, rewritten }
80
+ */
81
+ if (rename) {
82
+ router.patch('/plugins/:name', async (req, res) => {
83
+ if (!req.userEmail) {
84
+ res.status(401).json({ error: 'Unauthenticated' });
85
+ return;
86
+ }
87
+ const user = await resolveUser(req);
88
+ if (!user) {
89
+ res.status(401).json({ error: 'Unauthenticated' });
90
+ return;
91
+ }
92
+ const body = (req.body ?? {}) as { name?: unknown; displayName?: unknown };
93
+ try {
94
+ res.json(await rename.rename(user, String(req.params.name), { name: body.name, displayName: body.displayName }));
95
+ } catch (err) {
96
+ if (err instanceof PluginRenameError) {
97
+ res.status(err.status).json({ error: err.message, ...err.payload });
98
+ return;
99
+ }
100
+ if (err instanceof WorkflowDomainError) {
101
+ res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
102
+ return;
103
+ }
104
+ console.error('[plugins] rename failed:', err);
105
+ res.status(500).json({ error: 'Failed to rename the plugin' });
106
+ }
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Linking shared skills — see `PluginLinksService` for the two-sided write.
112
+ *
113
+ * POST /api/plugins/:name/links { skillPath } → { root, skills }
114
+ * DELETE /api/plugins/:name/links?skillPath= → { root, revoked }
115
+ * POST /api/plugins/:name/links/repair { skillPath } → { root }
116
+ *
117
+ * Refusals carry a `kind` the UI branches on — `needs-skill-write` (409) is
118
+ * the one that becomes "request write access".
119
+ */
120
+ if (links) {
121
+ const linkOp = async (
122
+ req: express.Request,
123
+ res: express.Response,
124
+ op: (user: AuthUser, plugin: string, skillPath: string) => Promise<unknown>,
125
+ skillPathOf: (req: express.Request) => unknown,
126
+ ) => {
127
+ if (!req.userEmail) {
128
+ res.status(401).json({ error: 'Unauthenticated' });
129
+ return;
130
+ }
131
+ const user = await resolveUser(req);
132
+ if (!user) {
133
+ res.status(401).json({ error: 'Unauthenticated' });
134
+ return;
135
+ }
136
+ const skillPath = skillPathOf(req);
137
+ if (typeof skillPath !== 'string' || !skillPath.trim()) {
138
+ res.status(400).json({ error: 'skillPath is required' });
139
+ return;
140
+ }
141
+ try {
142
+ res.json(await op(user, String(req.params.name), skillPath));
143
+ } catch (err) {
144
+ if (err instanceof PluginLinkError) {
145
+ res.status(err.status).json({ error: err.message, ...err.payload });
146
+ return;
147
+ }
148
+ if (err instanceof WorkflowDomainError) {
149
+ res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
150
+ return;
151
+ }
152
+ console.error('[plugins] link operation failed:', err);
153
+ res.status(500).json({ error: 'Failed to update the plugin\'s links' });
154
+ }
155
+ };
156
+ const bodyPath = (req: express.Request) => ((req.body ?? {}) as { skillPath?: unknown }).skillPath;
157
+ router.post('/plugins/:name/links', (req, res) => linkOp(req, res, (u, p, s) => links.link(u, p, s), bodyPath));
158
+ router.post('/plugins/:name/links/repair', (req, res) =>
159
+ linkOp(req, res, (u, p, s) => links.repair(u, p, s), bodyPath),
160
+ );
161
+ router.delete('/plugins/:name/links', (req, res) =>
162
+ linkOp(req, res, (u, p, s) => links.unlink(u, p, s), (r) => r.query.skillPath),
163
+ );
164
+ }
165
+
67
166
  /** The folder-chain probe for MEMBERSHIP — the folder itself. */
68
167
  const memberProbe = (folder: string) => folder;
69
168
  /** The FILE probe for discovery/management — the folder's access.md. */
70
169
  const accessMdOf = (folder: string) => `${folder}/access.md`;
170
+ /** A plugin folder's path BELOW the plugins root: `GTM`, or `teams/deep`. */
171
+ const folderBelowRoot = (folder: string) => folder.slice(folder.indexOf('/') + 1);
172
+ /**
173
+ * What a join request is keyed by: the plugin's primary FOLDER path below
174
+ * the root, not its identity. The request writes into that folder's rules;
175
+ * a top-level folder keys exactly as every join branch already on a remote
176
+ * was cut (its name), a nested one by its whole path, so two plugins whose
177
+ * folders share a basename never share a branch; and a rename of the
178
+ * identity moves no folder, so it orphans no open request.
179
+ */
180
+ const joinKeyOf = (g: PluginCatalogEntry) => folderBelowRoot(g.folders[0]);
71
181
 
72
182
  const probesFor = (plugins: PluginCatalogEntry[]): string[] => [
73
183
  ...new Set(plugins.flatMap((g) => g.folders.flatMap((f) => [memberProbe(f), accessMdOf(f)]))),
@@ -122,15 +232,18 @@ export function createPluginsRoutes(
122
232
  const owner = any(owned, g, memberProbe);
123
233
  const discoverable = member || any(readable, g, accessMdOf);
124
234
  if (!member && !manager && !discoverable) continue; // absent — fail closed
125
- const joinCr = member ? null : openJoinCr(mine, email, g.name);
235
+ const joinCr = member ? null : openJoinCr(mine, email, joinKeyOf(g));
126
236
  plugins.push({
127
237
  name: g.name,
238
+ displayName: g.displayName,
128
239
  folders: g.folders,
129
240
  canRead: member,
130
241
  canWrite: manager,
131
242
  isOwner: owner,
243
+ linksAreManaged: g.linksAreManaged,
132
244
  skillCount: g.skillCount,
133
245
  toolCount: g.toolCount,
246
+ brokenLinks: g.brokenLinks,
134
247
  owners: g.owners,
135
248
  writers: g.writers,
136
249
  readers: g.readers,
@@ -230,7 +343,7 @@ export function createPluginsRoutes(
230
343
  res.status(401).json({ error: 'Unauthenticated' });
231
344
  return;
232
345
  }
233
- // Case-sensitive, like `pluginOfPath` — the plugin name IS the folder name.
346
+ // By identity — the manifest name the catalog keys on.
234
347
  const plugin = (await pluginIndex.catalog()).find((g) => g.name === req.params.name);
235
348
  const wsId = pluginsWorkspaceId();
236
349
  const ownerVerdicts = plugin
@@ -242,7 +355,10 @@ export function createPluginsRoutes(
242
355
  res.status(404).json({ error: 'Unknown plugin', kind: 'unknown-plugin' });
243
356
  return;
244
357
  }
245
- await provision.deletePlugin(user, plugin.name);
358
+ // Provisioning works on FOLDERS (it created one); the identity only
359
+ // found the plugin. The whole path below the root, so a nested plugin
360
+ // is deleted where it is.
361
+ await provision.deletePlugin(user, folderBelowRoot(plugin.folders[0]));
246
362
  pluginIndex.invalidate();
247
363
  res.json({ ok: true });
248
364
  } catch (err) {
@@ -268,7 +384,7 @@ export function createPluginsRoutes(
268
384
  return;
269
385
  }
270
386
  const catalog = await pluginIndex.catalog();
271
- // Case-sensitive, like `pluginOfPath` — the plugin name IS the folder name.
387
+ // By identity — the manifest name the catalog keys on.
272
388
  const plugin = catalog.find((g) => g.name === req.params.name);
273
389
  const wsId = pluginsWorkspaceId();
274
390
  // Same ANY-folder shape `GET /plugins` resolves with, so a plugin can
@@ -298,8 +414,8 @@ export function createPluginsRoutes(
298
414
  // expects.
299
415
  const folder = plugin.folders[0];
300
416
 
301
- const branch = joinBranchFor(email, plugin.name);
302
- const existing = openJoinCr(await workflow.listChangeRequestsAuthoredBy(email), email, plugin.name);
417
+ const branch = joinBranchFor(email, joinKeyOf(plugin));
418
+ const existing = openJoinCr(await workflow.listChangeRequestsAuthoredBy(email), email, joinKeyOf(plugin));
303
419
  if (existing) {
304
420
  res.json({ ok: true, number: existing.number });
305
421
  return;
@@ -323,14 +439,15 @@ export function createPluginsRoutes(
323
439
  );
324
440
  if (spliced.changed) {
325
441
  await workspaceService.writeFile(ws.id, accessPath, spliced.text);
326
- await workflow.commitChanges(ws.id, user, `Request access to ${plugin.name}`);
442
+ await workflow.commitChanges(ws.id, user, `Request access to ${plugin.displayName}`);
327
443
  }
328
444
  const detail = await workflow.openChangeRequest(ws.id, user, {
329
445
  sourceBranch: branch,
330
446
  targetBranch: DEFAULT_BRANCH,
331
- title: `Join request: ${plugin.name}`,
447
+ // People read these: the display name, not the identifier.
448
+ title: `Join request: ${plugin.displayName}`,
332
449
  description:
333
- `${user.name} asked to join ${plugin.name}. A manager of the plugin accepts by ` +
450
+ `${user.name} asked to join ${plugin.displayName}. A manager of the plugin accepts by ` +
334
451
  `granting the access this branch proposes; the request closes itself once ` +
335
452
  `every proposal has landed.`,
336
453
  });
@@ -392,7 +509,7 @@ export function createPluginsRoutes(
392
509
  if (!ctx) return;
393
510
  const crs = await workflow.listChangeRequests();
394
511
  res.json({
395
- requests: await joinRequests.list(ctx.plugin.name, ctx.folder, crs, ctx.user),
512
+ requests: await joinRequests.list(joinKeyOf(ctx.plugin), ctx.folder, crs, ctx.user),
396
513
  });
397
514
  } catch (err) {
398
515
  console.error('[plugins] failed to list join requests:', err);
@@ -422,7 +539,7 @@ export function createPluginsRoutes(
422
539
  res.status(404).json({ error: 'Not found' });
423
540
  return;
424
541
  }
425
- res.json({ closed: await joinRequests.reconcile(ctx.plugin.name, ctx.folder, cr, ctx.user) });
542
+ res.json({ closed: await joinRequests.reconcile(joinKeyOf(ctx.plugin), ctx.folder, cr, ctx.user) });
426
543
  } catch (err) {
427
544
  console.error('[plugins] failed to reconcile a join request:', err);
428
545
  res.status(500).json({ error: 'Failed to update the request' });
@@ -1,11 +1,6 @@
1
1
  import path from 'node:path';
2
- import fs from 'node:fs/promises';
3
- import type { Dirent } from 'node:fs';
4
2
  import {
5
3
  DEFAULT_BRANCH,
6
- PLUGINS_DIR,
7
- pluginOfPath,
8
- isPersonalPluginFolder,
9
4
  } from '@bevel-software/platform-shared';
10
5
  import type { WorkspaceService } from '../workspace/workspace.service.js';
11
6
  import { workspaceIdForBranch } from '../../shared/workspace-id.js';
@@ -14,6 +9,9 @@ import type { ISkillService } from '../skills/skills.contract.js';
14
9
  import type { IToolManualService } from '../tool-manuals/tool-manuals.contract.js';
15
10
  import { TtlCache } from '../../shared/ttl-cache.js';
16
11
  import type { PluginCatalogEntry, IPluginIndexService } from './plugins.contract.js';
12
+ import type { PluginLinkIndex } from './plugin-links.js';
13
+ import type { PluginSource } from './discovery/plugin-source.js';
14
+ import { KbPluginSource } from './discovery/kb-plugin-source.js';
17
15
 
18
16
  const CACHE_TTL_MS = 60_000;
19
17
 
@@ -52,6 +50,14 @@ export class PluginIndexService implements IPluginIndexService {
52
50
  private readonly toolManualService: IToolManualService,
53
51
  private readonly kbDirName: string,
54
52
  now: () => number = Date.now,
53
+ /**
54
+ * The link index, when the deployment has one: a plugin's skill count is
55
+ * then inline PLUS linked. Optional so hosts composing their own service
56
+ * set (and older tests) keep the inline-only count.
57
+ */
58
+ private readonly links?: PluginLinkIndex,
59
+ /** Where plugins come from — native manifests unless a dialect is configured. */
60
+ private readonly source: PluginSource = new KbPluginSource(),
55
61
  ) {
56
62
  this.cache = new TtlCache(CACHE_TTL_MS, now);
57
63
  }
@@ -63,6 +69,9 @@ export class PluginIndexService implements IPluginIndexService {
63
69
  async catalog(): Promise<PluginCatalogEntry[]> {
64
70
  const cached = this.cache.get();
65
71
  if (cached) return cached;
72
+ // See `TtlCache.begin`: taken before the read so an `invalidate()` that
73
+ // lands mid-build discards this result instead of being overwritten by it.
74
+ const token = this.cache.begin();
66
75
  const entries = await this.build();
67
76
  // A failed scan and a KB with genuinely no plugin folders both serve `[]`,
68
77
  // but only the second is a fact worth holding for the TTL. Caching the
@@ -72,7 +81,7 @@ export class PluginIndexService implements IPluginIndexService {
72
81
  // never arrive in that window. So a degraded read is served, not stored,
73
82
  // and the next caller retries.
74
83
  if (entries === null) return [];
75
- this.cache.set(entries);
84
+ this.cache.set(entries, token);
76
85
  return entries;
77
86
  }
78
87
 
@@ -94,12 +103,13 @@ export class PluginIndexService implements IPluginIndexService {
94
103
  const wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
95
104
  const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
96
105
 
97
- const folders = await this.scanFolders(kbRoot);
98
- if (folders.size === 0) return [];
106
+ const scanned = await this.scanFolders(kbRoot);
107
+ if (scanned.size === 0) return [];
108
+ const folders = new Map([...scanned].map(([name, p]) => [name, p.folders]));
99
109
 
100
- const [skillCounts, toolCounts] = await Promise.all([
101
- this.countSkills(),
102
- this.countTools(),
110
+ const [{ skillCounts, brokenLinkCounts }, toolCounts] = await Promise.all([
111
+ this.countThroughLinks(folders),
112
+ this.countTools(folders),
103
113
  ]);
104
114
 
105
115
  const entries: PluginCatalogEntry[] = [];
@@ -113,9 +123,12 @@ export class PluginIndexService implements IPluginIndexService {
113
123
  ]);
114
124
  entries.push({
115
125
  name,
126
+ displayName: scanned.get(name)?.displayName ?? name,
116
127
  folders: pluginFolders,
128
+ linksAreManaged: scanned.get(name)?.linksAreManaged ?? false,
117
129
  skillCount: skillCounts.get(name) ?? 0,
118
130
  toolCount: toolCounts.get(name) ?? 0,
131
+ brokenLinks: brokenLinkCounts.get(name) ?? 0,
119
132
  owners,
120
133
  writers,
121
134
  readers,
@@ -130,59 +143,99 @@ export class PluginIndexService implements IPluginIndexService {
130
143
  }
131
144
  }
132
145
 
133
- /** name → repo-relative plugin folder (always a single-element list). */
134
- private async scanFolders(kbRoot: string): Promise<Map<string, string[]>> {
135
- const byName = new Map<string, string[]>();
136
- let children: Dirent[];
137
- try {
138
- children = await fs.readdir(path.join(kbRoot, PLUGINS_DIR), { withFileTypes: true });
139
- } catch {
140
- return byName; // a KB without a `Plugins/` root simply has no plugins
146
+ /**
147
+ * name → repo-relative plugin folder (always a single-element list), from
148
+ * the configured source. Personal folders live under Plugins/ but are not
149
+ * plugins: one exists per person, private by construction, and listing them
150
+ * would put a locked row per employee in everyone's index. `exists` is the
151
+ * source's existence rule (see `DiscoveredPlugin`) — for native plugins,
152
+ * the `access.md` the class doc describes.
153
+ */
154
+ private async scanFolders(
155
+ kbRoot: string,
156
+ ): Promise<Map<string, { folders: string[]; linksAreManaged: boolean; displayName: string }>> {
157
+ const byName = new Map<string, { folders: string[]; linksAreManaged: boolean; displayName: string }>();
158
+ const discovered = await this.source.discover(kbRoot);
159
+ for (const w of discovered.warnings) console.warn(`[plugins] ${w}`);
160
+ for (const plugin of discovered.plugins) {
161
+ if (plugin.personal || !plugin.exists) continue;
162
+ byName.set(plugin.name, {
163
+ folders: [plugin.folder],
164
+ linksAreManaged: plugin.linksAreManaged,
165
+ displayName: plugin.displayName,
166
+ });
141
167
  }
142
- const candidates = children.filter(
143
- (child) =>
144
- child.isDirectory() &&
145
- !child.name.startsWith('.') &&
146
- // Personal folders live under Plugins/ but are not plugins: one exists
147
- // per person, private by construction, and listing them would put a
148
- // locked row per employee in everyone's index.
149
- !isPersonalPluginFolder(child.name),
150
- );
151
- // A plugin exists exactly when its folder carries an `access.md` (see the
152
- // class doc) — stat that file, don't trust the directory.
153
- const verdicts = await Promise.all(
154
- candidates.map(async (child) => {
155
- try {
156
- return (await fs.stat(path.join(kbRoot, PLUGINS_DIR, child.name, 'access.md'))).isFile();
157
- } catch {
158
- return false;
159
- }
160
- }),
161
- );
162
- candidates.forEach((child, i) => {
163
- if (verdicts[i]) byName.set(child.name, [`${PLUGINS_DIR}/${child.name}`]);
164
- });
165
168
  return byName;
166
169
  }
167
170
 
168
- private async countSkills(): Promise<Map<string, number>> {
169
- // `undefined` is the documented GLOBAL, unfiltered mode — counts are a
170
- // property of the plugin, not of who is asking.
171
- return bucketByPlugin(await this.skillService.listSkills(undefined));
171
+ /**
172
+ * Two counts from ONE read of the link index — the membership is built
173
+ * once per cold catalog, not once per count: `TtlCache` has no
174
+ * single-flight, so two concurrent readers of an empty cache would each
175
+ * discover the tree and resolve every linked skill's access.
176
+ *
177
+ * - `skillCounts`: with links, a skill counts for EVERY plugin that holds
178
+ * it — inline in its folder, or linked from a manifest. Personal folders
179
+ * are already absent from the membership (they are places, not plugins).
180
+ * - `brokenLinkCounts`: how many linked skills each plugin's members
181
+ * cannot read — memberships reported as linked but not granted. From the
182
+ * UNFILTERED index, so the count reaches the plugin's managers even when
183
+ * the missing grant locks them out of the skill too.
184
+ *
185
+ * Without a link index: skills bucket by folder, and no link can be broken.
186
+ */
187
+ private async countThroughLinks(
188
+ folders: Map<string, string[]>,
189
+ ): Promise<{ skillCounts: Map<string, number>; brokenLinkCounts: Map<string, number> }> {
190
+ const skillCounts = new Map<string, number>();
191
+ const brokenLinkCounts = new Map<string, number>();
192
+ if (!this.links) {
193
+ // `undefined` is the documented GLOBAL, unfiltered mode — counts are a
194
+ // property of the plugin, not of who is asking.
195
+ return {
196
+ skillCounts: bucketByFolder(await this.skillService.listSkills(undefined), folders),
197
+ brokenLinkCounts,
198
+ };
199
+ }
200
+ for (const memberships of (await this.links.membership()).bySkill.values()) {
201
+ for (const m of memberships) {
202
+ skillCounts.set(m.name, (skillCounts.get(m.name) ?? 0) + 1);
203
+ if (m.linked && !m.granted) brokenLinkCounts.set(m.name, (brokenLinkCounts.get(m.name) ?? 0) + 1);
204
+ }
205
+ }
206
+ return { skillCounts, brokenLinkCounts };
172
207
  }
173
208
 
174
- private async countTools(): Promise<Map<string, number>> {
175
- return bucketByPlugin(await this.toolManualService.listAllSummaries());
209
+ /**
210
+ * Tools belong to the plugin whose FOLDER holds them — matched by path
211
+ * prefix against the discovered folders, not by `pluginOfPath`'s
212
+ * second-segment rule, so a dialect plugin nested three folders deep still
213
+ * counts the servers its bundle expands.
214
+ */
215
+ private async countTools(folders: Map<string, string[]>): Promise<Map<string, number>> {
216
+ const counts = new Map<string, number>();
217
+ const byFolder = [...folders.entries()].map(([name, [folder]]) => ({ name, prefix: `${folder}/` }));
218
+ for (const tool of await this.toolManualService.listAllSummaries()) {
219
+ const owner = byFolder.find((f) => tool.path.startsWith(f.prefix));
220
+ if (!owner) continue;
221
+ counts.set(owner.name, (counts.get(owner.name) ?? 0) + 1);
222
+ }
223
+ return counts;
176
224
  }
177
225
  }
178
226
 
179
- /** Count items per plugin folder name; ungrouped items (`null`) count nowhere. */
180
- function bucketByPlugin(items: { path: string }[]): Map<string, number> {
227
+ /**
228
+ * Count items per plugin by FOLDER PREFIX against the discovered folders —
229
+ * not by `pluginOfPath`'s second-segment rule, which would file a nested
230
+ * plugin's skills under its grouping folder. Items in no plugin count nowhere.
231
+ */
232
+ function bucketByFolder(items: { path: string }[], folders: Map<string, string[]>): Map<string, number> {
181
233
  const counts = new Map<string, number>();
234
+ const byFolder = [...folders.entries()].map(([name, [folder]]) => ({ name, prefix: `${folder}/` }));
182
235
  for (const item of items) {
183
- const plugin = pluginOfPath(item.path);
184
- if (plugin === null) continue;
185
- counts.set(plugin, (counts.get(plugin) ?? 0) + 1);
236
+ const owner = byFolder.find((f) => item.path.startsWith(f.prefix));
237
+ if (!owner) continue;
238
+ counts.set(owner.name, (counts.get(owner.name) ?? 0) + 1);
186
239
  }
187
240
  return counts;
188
241
  }
@@ -76,6 +76,10 @@ async function baseUrlWith(auth: { userId?: string; email?: string }): Promise<s
76
76
  secretsVault,
77
77
  toolManualService,
78
78
  accessControl,
79
+ // These tests are about the credential write, not the probe.
80
+ connectionProbe: {
81
+ probe: async () => ({ status: 'unverifiable' as const, detail: null, checkedAt: new Date() }),
82
+ },
79
83
  stateSecret: 'test-secret',
80
84
  publicBackendUrl: 'http://localhost:3000',
81
85
  publicFrontendUrl: 'http://localhost:5173',
@@ -174,6 +178,10 @@ describe('GET /api/connect/pending — OAuth scope coverage', () => {
174
178
  secretsVault: vaultWithGranted(grantedScopes),
175
179
  toolManualService: oauthTool,
176
180
  accessControl,
181
+ // These tests are about the credential write, not the probe.
182
+ connectionProbe: {
183
+ probe: async () => ({ status: 'unverifiable' as const, detail: null, checkedAt: new Date() }),
184
+ },
177
185
  stateSecret: 'test-secret',
178
186
  publicBackendUrl: 'http://localhost:3000',
179
187
  publicFrontendUrl: 'http://localhost:5173',
@@ -257,6 +265,10 @@ describe('GET /api/connect/pending — tool sign-ins are not double-listed as st
257
265
  secretsVault: vault,
258
266
  toolManualService: oauthTool,
259
267
  accessControl,
268
+ // These tests are about the credential write, not the probe.
269
+ connectionProbe: {
270
+ probe: async () => ({ status: 'unverifiable' as const, detail: null, checkedAt: new Date() }),
271
+ },
260
272
  stateSecret: 'test-secret',
261
273
  publicBackendUrl: 'http://localhost:3000',
262
274
  publicFrontendUrl: 'http://localhost:5173',
@@ -31,8 +31,7 @@ const TOOL_PATH = 'Tools/weather.tool';
31
31
  const USER = 'user@x.com';
32
32
  const RETURN_TO = '/skills-and-tools/tools/weather';
33
33
 
34
- const toolManualService = {
35
- listAccessible: async () => [
34
+ const MANUALS = [
36
35
  {
37
36
  slug: 'weather',
38
37
  name: 'weather',
@@ -51,7 +50,10 @@ const toolManualService = {
51
50
  },
52
51
  ],
53
52
  },
54
- ],
53
+ ];
54
+
55
+ const toolManualService = {
56
+ listAccessible: async () => MANUALS,
55
57
  } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['toolManualService'];
56
58
 
57
59
  const accessControl = {
@@ -65,10 +67,15 @@ const secretsVault = {
65
67
  completeOAuth,
66
68
  } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['secretsVault'];
67
69
 
70
+ const connectionProbe = {
71
+ probe: async () => ({ status: 'unverifiable' as const, detail: null, checkedAt: new Date() }),
72
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['connectionProbe'];
73
+
68
74
  const deps = {
69
75
  secretsVault,
70
76
  toolManualService,
71
77
  accessControl,
78
+ connectionProbe,
72
79
  stateSecret: STATE_SECRET,
73
80
  publicBackendUrl: 'http://localhost:3000',
74
81
  publicFrontendUrl: FRONTEND,
@@ -28,6 +28,9 @@ const toolManualService = {
28
28
  setup: { kind: 'oauth-manual' as const, reason: 'no authorization-server metadata at https://weather.example' },
29
29
  variables: [
30
30
  { name: 'SHARED_KEY', scope: 'admin' as const, label: 'Org key' },
31
+ // A plain per-user key, so the user-scoped write path (and the
32
+ // per-user invalidation it owes) is exercised alongside the shared one.
33
+ { name: 'MY_KEY', scope: 'user' as const, label: 'Your key' },
31
34
  {
32
35
  name: 'SIGNIN',
33
36
  scope: 'user' as const,
@@ -56,9 +59,11 @@ const toolManualService = {
56
59
  ],
57
60
  } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['toolManualService'];
58
61
 
62
+ const putStatic = vi.fn(async () => ({ id: 'u1' }));
59
63
  const putSharedStatic = vi.fn(async () => ({ id: 's1' }));
60
64
  const putSharedOAuthClientSecret = vi.fn(async () => {});
61
65
  const secretsVault = {
66
+ putStatic,
62
67
  putSharedStatic,
63
68
  putSharedOAuthClientSecret,
64
69
  } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['secretsVault'];
@@ -69,6 +74,10 @@ const accessControl = {
69
74
  canWrite: async (_ws: string, email: string, path: string) => email === WRITER && path === TOOL_PATH,
70
75
  } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['accessControl'];
71
76
 
77
+ const connectionProbe = {
78
+ probe: async () => ({ status: 'unverifiable' as const, detail: null, checkedAt: new Date() }),
79
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0]['connectionProbe'];
80
+
72
81
  let httpServer: HttpServer | undefined;
73
82
 
74
83
  async function baseUrlAs(email: string): Promise<string> {
@@ -85,6 +94,7 @@ async function baseUrlAs(email: string): Promise<string> {
85
94
  secretsVault,
86
95
  toolManualService,
87
96
  accessControl,
97
+ connectionProbe,
88
98
  stateSecret: 'test-secret',
89
99
  publicBackendUrl: 'http://localhost:3000',
90
100
  publicFrontendUrl: 'http://localhost:5173',
@@ -102,6 +112,7 @@ afterEach(async () => {
102
112
  httpServer = undefined;
103
113
  putSharedStatic.mockClear();
104
114
  putSharedOAuthClientSecret.mockClear();
115
+ putStatic.mockClear();
105
116
  });
106
117
 
107
118
  describe('tool owner gate — shared config requires WRITE on the `.tool` file', () => {
@@ -116,6 +127,21 @@ describe('tool owner gate — shared config requires WRITE on the `.tool` file',
116
127
  expect(putSharedStatic).toHaveBeenCalledWith(expect.objectContaining({ key: 'weather_SHARED_KEY' }));
117
128
  });
118
129
 
130
+ it('a mere READER may still set their OWN value for the same tool', async () => {
131
+ // The gate is on the SHARED value only. One person's own key says nothing
132
+ // about anyone else's, so needing write access to the `.tool` file to type
133
+ // your own credential would lock every reader out of the tools they can see.
134
+ const base = await baseUrlAs(READER);
135
+ const res = await fetch(`${base}/api/secrets/tools/weather/vars/MY_KEY/user`, {
136
+ method: 'PUT',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify({ value: 'mine-123' }),
139
+ });
140
+ expect(res.status).toBe(201);
141
+ expect(putStatic).toHaveBeenCalledWith(expect.objectContaining({ key: 'weather_MY_KEY' }));
142
+ expect(putSharedStatic).not.toHaveBeenCalled();
143
+ });
144
+
119
145
  it('a non-writer is refused (403), even though they can READ the tool', async () => {
120
146
  const base = await baseUrlAs(READER);
121
147
  const res = await fetch(`${base}/api/secrets/tools/weather/vars/SHARED_KEY/admin`, {
@@ -97,7 +97,7 @@ export class DbSecretsVaultService implements ISecretsVaultService {
97
97
  if (!this.cryptoInstance) {
98
98
  if (!this.encKey) {
99
99
  throw new InvalidSecretError(
100
- 'Secrets require an encryption key — set CONNECTOR_CONFIG_ENC_KEY (or SHAREPOINT_TOKEN_ENC_KEY).',
100
+ 'Secrets require an encryption key — set SECRETS_ENC_KEY.',
101
101
  );
102
102
  }
103
103
  this.cryptoInstance = new TokenCrypto(this.encKey);