@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
@@ -10,6 +10,7 @@ import {
10
10
  type ISecretsVaultService,
11
11
  } from './secrets-vault.contract.js';
12
12
  import type { IToolManualService, ToolManualSummary, ToolVariable } from '../tool-manuals/tool-manuals.contract.js';
13
+ import type { IConnectionProbeService } from '../connection-probe/connection-probe.contract.js';
13
14
  import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
14
15
  import type { IAccessControl } from '../access/access-control.interface.js';
15
16
  import { workspaceIdForBranch } from '../../shared/workspace-id.js';
@@ -21,6 +22,11 @@ export interface SecretsVaultRoutesDeps {
21
22
  toolManualService: IToolManualService;
22
23
  /** Gates who may set a tool's ADMIN (shared) secrets — writers of the `.tool` file. */
23
24
  accessControl: IAccessControl;
25
+ /**
26
+ * Verdicts of the last credential PROBE per tool — what makes the difference
27
+ * between "a key is stored" and "the key works" visible to the UI.
28
+ */
29
+ connectionProbe: IConnectionProbeService;
24
30
  /** HMAC secret for signing the OAuth `state` (reuse the connector state secret). */
25
31
  stateSecret: string;
26
32
  /** Public base URL of THIS backend — builds the OAuth redirect URI. */
@@ -69,7 +75,7 @@ export function isSafeReturnPath(returnTo: unknown): returnTo is string {
69
75
  * `req.userId` — secrets are private per user. Mounted behind the JWT middleware.
70
76
  */
71
77
  export function createSecretsVaultRoutes(deps: SecretsVaultRoutesDeps): express.Router {
72
- const { secretsVault, toolManualService, accessControl } = deps;
78
+ const { secretsVault, toolManualService, accessControl, connectionProbe } = deps;
73
79
  const router = express.Router();
74
80
  // A FUNCTION, not a constant. Routers are constructed at boot, and on a
75
81
  // deployment configured through the setup screen the branch model does not
@@ -361,6 +367,10 @@ export function createSecretsVaultRoutes(deps: SecretsVaultRoutesDeps): express.
361
367
  value: body.value,
362
368
  label: body.label,
363
369
  });
370
+ // No probe here, and nothing to invalidate: a verdict is never stored, so
371
+ // saving a key cannot leave a stale one behind. The caller follows this
372
+ // with an explicit check, which keeps the save fast and gives the UI a
373
+ // "Testing…" state to show instead of a frozen dialog.
364
374
  res.status(201).json({ secret });
365
375
  } catch (err) {
366
376
  mapError(err, res, 'set user var');
@@ -504,6 +514,30 @@ export function createSecretsVaultRoutes(deps: SecretsVaultRoutesDeps): express.
504
514
  }
505
515
  });
506
516
 
517
+ /**
518
+ * Probe one tool's credential NOW and return the verdict.
519
+ *
520
+ * A POST because it has an effect on the world — it makes a real
521
+ * authenticated call to the provider — and because browsers and proxies are
522
+ * free to cache a GET, which for a freshness check would defeat the point.
523
+ *
524
+ * A probe outcome is never an HTTP error: a rejected credential is a
525
+ * successful check that found a problem, so it comes back 200 with
526
+ * `status: 'failed'`. Only a tool that can't be found or read 404s.
527
+ */
528
+ router.post('/secrets/tools/:slug/check', async (req, res) => {
529
+ const userId = req.userId;
530
+ const email = req.userEmail;
531
+ if (!userId || !email) return void res.status(401).json({ error: 'Not authenticated' });
532
+ try {
533
+ const verdict = await connectionProbe.probe(userId, email, req.params.slug);
534
+ if (!verdict) return void res.status(404).json({ error: 'Tool not found' });
535
+ res.json({ verdict });
536
+ } catch (err) {
537
+ mapError(err, res, 'check tool connection');
538
+ }
539
+ });
540
+
507
541
  return router;
508
542
  }
509
543
 
@@ -150,6 +150,63 @@ describe('DeploymentSettingsService — secrets', () => {
150
150
  });
151
151
  });
152
152
 
153
+ describe('DeploymentSettingsService — KB layout', () => {
154
+ it('resolves the three roots to their defaults when nothing names them', () => {
155
+ const { db } = makeDb();
156
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
157
+ expect(settings.resolveKbLayout()).toEqual({
158
+ knowledgeBaseDir: 'KnowledgeBase',
159
+ skillsDir: 'Skills',
160
+ pluginsDir: 'Plugins',
161
+ });
162
+ });
163
+
164
+ it('takes a renamed root from the environment first, then the store', async () => {
165
+ const { db } = makeDb();
166
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
167
+ await settings.save({ skillsDir: 'skills', pluginsDir: 'plugins' }, null);
168
+ expect(settings.resolveKbLayout()).toMatchObject({ skillsDir: 'skills', pluginsDir: 'plugins' });
169
+ process.env.KB_SKILLS_DIR = 'capabilities';
170
+ expect(settings.resolveKbLayout().skillsDir).toBe('capabilities');
171
+ });
172
+
173
+ /**
174
+ * The trio rule is judged on the layout the save WOULD produce: a plugins
175
+ * folder renamed to collide with the skills folder already in effect is a
176
+ * collision even though the batch names only one of them.
177
+ */
178
+ it('refuses two roots that share a name, case-insensitively', async () => {
179
+ const { db } = makeDb();
180
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
181
+ await expect(settings.save({ pluginsDir: 'skills' }, null)).rejects.toBeInstanceOf(
182
+ SettingsValidationError,
183
+ );
184
+ await expect(settings.save({ pluginsDir: 'SKILLS' }, null)).rejects.toBeInstanceOf(
185
+ SettingsValidationError,
186
+ );
187
+ await expect(
188
+ settings.save({ knowledgeBaseDir: 'Content', skillsDir: 'content' }, null),
189
+ ).rejects.toBeInstanceOf(SettingsValidationError);
190
+ });
191
+
192
+ it('refuses a root that is not a single plain folder name', async () => {
193
+ const { db } = makeDb();
194
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
195
+ for (const bad of ['a/b', '..', '.hidden', 'x\\y']) {
196
+ await expect(settings.save({ skillsDir: bad }, null)).rejects.toBeInstanceOf(
197
+ SettingsValidationError,
198
+ );
199
+ }
200
+ });
201
+
202
+ it('marks a layout change as restart-to-apply', async () => {
203
+ const { db } = makeDb();
204
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
205
+ const { restartRequired } = await settings.save({ pluginsDir: 'Bundles' }, null);
206
+ expect(restartRequired).toBe(true);
207
+ });
208
+ });
209
+
153
210
  describe('DeploymentSettingsService — validation', () => {
154
211
  it('rejects the whole batch when one field is wrong', async () => {
155
212
  const { db, rows } = makeDb();
@@ -0,0 +1,77 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import { randomBytes } from 'node:crypto';
4
+ import express from 'express';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ import { createSetupRoutes, type LastSyncStatus } from '../setup.routes.js';
7
+ import { DeploymentSettingsService } from '../deployment-settings.service.js';
8
+ import type { IAdminAccessService } from '../../admin/admin.interface.js';
9
+ import type { Database } from '../../database/connection.js';
10
+
11
+ /**
12
+ * The remote-sync facts on `GET /setup/status`: the address a hook calls and
13
+ * what the last sync did. Admin-only like the rest of the settings — a
14
+ * non-admin learns nothing about the deployment's endpoints.
15
+ */
16
+ const ENC_KEY = randomBytes(32).toString('base64');
17
+
18
+ let server: HttpServer | null = null;
19
+ afterEach(() => {
20
+ server?.close();
21
+ server = null;
22
+ });
23
+
24
+ function listen(isAdmin: boolean, last: LastSyncStatus | null) {
25
+ const db = {
26
+ select: () => ({ from: () => Promise.resolve([]) }),
27
+ insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
28
+ delete: () => ({ where: () => Promise.resolve() }),
29
+ } as unknown as Database;
30
+ const settings = new DeploymentSettingsService(db, ENC_KEY);
31
+ const app = express();
32
+ app.use(express.json());
33
+ app.use((req, _res, next) => {
34
+ req.userEmail = 'root@example.com';
35
+ req.userId = 'user-1';
36
+ next();
37
+ });
38
+ app.use(
39
+ '/api',
40
+ createSetupRoutes(
41
+ settings,
42
+ { isAdmin: async () => isAdmin } as IAdminAccessService,
43
+ { runAll: async () => {} },
44
+ { url: 'https://hexis.example.test/api/sync', lastSync: () => last },
45
+ ),
46
+ );
47
+ server = app.listen(0);
48
+ const { port } = server.address() as AddressInfo;
49
+ return `http://127.0.0.1:${port}`;
50
+ }
51
+
52
+ describe('GET /setup/status — remote sync', () => {
53
+ it('tells an admin the address and the last sync', async () => {
54
+ const last: LastSyncStatus = {
55
+ at: 1_700_000_000_000,
56
+ by: 'bearer',
57
+ status: 'synced',
58
+ results: [{ branch: 'main', outcome: 'updated' }],
59
+ };
60
+ const res = await fetch(`${listen(true, last)}/api/setup/status`);
61
+ const body = (await res.json()) as { sync?: unknown };
62
+ expect(body.sync).toEqual({ url: 'https://hexis.example.test/api/sync', last });
63
+ });
64
+
65
+ it('says null before the first sync', async () => {
66
+ const res = await fetch(`${listen(true, null)}/api/setup/status`);
67
+ const body = (await res.json()) as { sync?: { last: unknown } };
68
+ expect(body.sync?.last).toBeNull();
69
+ });
70
+
71
+ it('tells a non-admin nothing', async () => {
72
+ const res = await fetch(`${listen(false, null)}/api/setup/status`);
73
+ const body = (await res.json()) as Record<string, unknown>;
74
+ expect(body.sync).toBeUndefined();
75
+ expect(body.settings).toBeUndefined();
76
+ });
77
+ });
@@ -1,7 +1,12 @@
1
1
  import { eq, inArray } from 'drizzle-orm';
2
2
  import type { Database } from '../database/connection.js';
3
3
  import { deploymentSettings } from '../database/core-schema.js';
4
- import { validateBranchModel } from '@bevel-software/platform-shared';
4
+ import {
5
+ DEFAULT_KB_LAYOUT,
6
+ validateBranchModel,
7
+ validateKbLayout,
8
+ validateKbRootName,
9
+ } from '@bevel-software/platform-shared';
5
10
  import { TokenCrypto } from '../../shared/token-crypto.js';
6
11
 
7
12
  /**
@@ -85,6 +90,38 @@ export const CORE_SETTINGS: SettingDef[] = [
85
90
  restartToApply: true,
86
91
  },
87
92
 
93
+ /**
94
+ * The KB layout: the three root folders a deployment may rename so hexis can
95
+ * read a repository laid out by someone else (`skills/` and `plugins/` in
96
+ * lowercase, say). Restart-to-apply like the branch model — the names are
97
+ * applied once at boot through `configureKbLayout` and served to the browser
98
+ * once by `/api/config`. Each has a default, so an unset field means the
99
+ * default, not an unconfigured deployment. Checked as a trio in `save`: the
100
+ * three must differ, and one field alone cannot see the other two.
101
+ */
102
+ {
103
+ key: 'knowledgeBaseDir',
104
+ envVar: 'KB_KNOWLEDGE_BASE_DIR',
105
+ section: 'knowledge-base',
106
+ validate: validateKbRootName,
107
+ restartToApply: true,
108
+ },
109
+ {
110
+ key: 'skillsDir',
111
+ envVar: 'KB_SKILLS_DIR',
112
+ section: 'knowledge-base',
113
+ validate: validateKbRootName,
114
+ restartToApply: true,
115
+ },
116
+ {
117
+ key: 'pluginsDir',
118
+ envVar: 'KB_PLUGINS_DIR',
119
+ section: 'knowledge-base',
120
+ validate: validateKbRootName,
121
+ restartToApply: true,
122
+ },
123
+
124
+
88
125
  /**
89
126
  * The branch model. Both are restart-to-apply and could not be otherwise:
90
127
  * the backend hands `DEFAULT_BRANCH` to services at construction, and the
@@ -109,6 +146,25 @@ export const CORE_SETTINGS: SettingDef[] = [
109
146
  restartToApply: true,
110
147
  },
111
148
 
149
+ {
150
+ /**
151
+ * The credential a git host's webhook or a pipeline presents to
152
+ * `POST /api/sync` (see `modules/kb-sync/`). Deployment-level on purpose:
153
+ * a person's connection key would stop the pipeline the day they leave.
154
+ * Optional — without it the endpoint only admits an admin's own session.
155
+ * Read per request, so it applies without a restart.
156
+ */
157
+ key: 'kbSyncSecret',
158
+ envVar: 'KB_SYNC_SECRET',
159
+ section: 'knowledge-base',
160
+ secret: true,
161
+ // The same floor `sync-auth.ts` enforces at request time (which is what
162
+ // covers a secret set through the environment); this one just says so
163
+ // before the save.
164
+ validate: (v) =>
165
+ v.trim().length >= 16 ? null : 'Use at least 16 characters — a random string is best.',
166
+ },
167
+
112
168
  /**
113
169
  * Single sign-on. Restart-to-apply because the provider is built once at boot
114
170
  * and pushed into the auth plugin array the server mounts from.
@@ -266,6 +322,18 @@ export class DeploymentSettingsService {
266
322
  return (this.stored.get(key) ?? '').trim();
267
323
  }
268
324
 
325
+ /**
326
+ * The KB layout in effect: each root from the environment, else the stored
327
+ * row, else its default. The composition root applies this once at boot.
328
+ */
329
+ resolveKbLayout(): { knowledgeBaseDir: string; skillsDir: string; pluginsDir: string } {
330
+ return {
331
+ knowledgeBaseDir: this.resolve('knowledgeBaseDir') || DEFAULT_KB_LAYOUT.knowledgeBaseDir,
332
+ skillsDir: this.resolve('skillsDir') || DEFAULT_KB_LAYOUT.skillsDir,
333
+ pluginsDir: this.resolve('pluginsDir') || DEFAULT_KB_LAYOUT.pluginsDir,
334
+ };
335
+ }
336
+
269
337
  /** Where {@link resolve} got its answer — what the setup screen labels the field with. */
270
338
  sourceOf(key: string): SettingSource {
271
339
  const def = this.defs.get(key);
@@ -356,11 +424,36 @@ export class DeploymentSettingsService {
356
424
  if (problem) problems.protectedBranches = problem;
357
425
  }
358
426
 
427
+ // The layout trio is the other cross-field rule: three names that must
428
+ // differ. Judged on the layout this save WOULD produce, with the default
429
+ // standing in for anything neither written nor stored.
430
+ const layoutKeys = ['knowledgeBaseDir', 'skillsDir', 'pluginsDir'] as const;
431
+ if (toWrite.some((w) => (layoutKeys as readonly string[]).includes(w.key))) {
432
+ const effective = (key: (typeof layoutKeys)[number]) =>
433
+ toWrite.find((w) => w.key === key)?.value || this.resolve(key) || DEFAULT_KB_LAYOUT[key];
434
+ const problem = validateKbLayout({
435
+ knowledgeBaseDir: effective('knowledgeBaseDir'),
436
+ skillsDir: effective('skillsDir'),
437
+ pluginsDir: effective('pluginsDir'),
438
+ });
439
+ // Against the field being written — the first one in the batch — since
440
+ // any of the three could be the one that collides.
441
+ if (problem) {
442
+ const first = toWrite.find((w) => (layoutKeys as readonly string[]).includes(w.key))!;
443
+ problems[first.key] = problem;
444
+ }
445
+ }
446
+
359
447
  if (Object.keys(problems).length > 0) throw new SettingsValidationError(problems);
360
448
 
361
449
  let restartRequired = false;
362
450
  for (const { key, value, def } of toWrite) {
363
- if (def.restartToApply && this.resolve(key) !== value) restartRequired = true;
451
+ // Compared against the EFFECTIVE value: a layout root that was unset
452
+ // was already running on its default, so saving that default changes
453
+ // nothing a restart would pick up.
454
+ const effective =
455
+ this.resolve(key) || (DEFAULT_KB_LAYOUT as Record<string, string | undefined>)[key] || '';
456
+ if (def.restartToApply && effective !== value) restartRequired = true;
364
457
  const encrypted = def.secret === true;
365
458
  const stored = encrypted ? this.crypto!.encrypt(value) : value;
366
459
  await this.db
@@ -16,6 +16,18 @@ import '../auth/auth.middleware.js'; // Express Request augmentation
16
16
 
17
17
  const execFileAsync = promisify(execFile);
18
18
 
19
+ /**
20
+ * The slice of a sync record the status endpoint publishes. Declared here
21
+ * rather than imported from `kb-sync` so this module stays free of that one;
22
+ * the composition root hands in something that satisfies it.
23
+ */
24
+ export interface LastSyncStatus {
25
+ at: number;
26
+ by: string;
27
+ status: 'synced' | 'partial';
28
+ results: Array<{ branch: string; outcome: string; error?: string }>;
29
+ }
30
+
19
31
  /**
20
32
  * First-run setup — the deployment's own configuration, for the things that
21
33
  * used to be environment-only.
@@ -37,6 +49,17 @@ export function createSetupRoutes(
37
49
  * would race.
38
50
  */
39
51
  kbStartupRunner: { runAll(): Promise<void> },
52
+ /**
53
+ * The remote-sync facts the Deployment page shows beside the sync secret:
54
+ * the address a webhook or pipeline must call, and what the last call did.
55
+ * Optional so a minimal mount (tests, a distribution without the module)
56
+ * simply omits them from the status.
57
+ */
58
+ sync?: {
59
+ /** `<PUBLIC_BACKEND_URL>/api/sync` — the reader appends `/<branch>`. */
60
+ url: string;
61
+ lastSync(): LastSyncStatus | null;
62
+ },
40
63
  ): express.Router {
41
64
  const router = express.Router();
42
65
 
@@ -101,6 +124,7 @@ export function createSetupRoutes(
101
124
  isAdmin: true,
102
125
  settings: settings.describe(),
103
126
  ...(kbInitFailed ? { kbInitError } : {}),
127
+ ...(sync ? { sync: { url: sync.url, last: sync.lastSync() } } : {}),
104
128
  });
105
129
  });
106
130
 
@@ -0,0 +1,128 @@
1
+ import type { Server } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import express from 'express';
4
+ import { describe, it, expect, afterEach, vi } from 'vitest';
5
+ import { DEFAULT_BRANCH, joinBranchFor } from '@bevel-software/platform-shared';
6
+ import type { IWorkflowService } from '@bevel-software/platform-shared';
7
+
8
+ import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
9
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
10
+ import type { IAccessControl } from '../../access/access-control.interface.js';
11
+ import type { JoinRequestsService } from '../../plugins/join-requests.service.js';
12
+ import type { ISkillService } from '../skills.contract.js';
13
+ import { createSkillAccessRequestRoutes } from '../skill-access-requests.routes.js';
14
+
15
+ /**
16
+ * The branch probe behind an access request: the listing is used to PROVE a
17
+ * branch absent before one is cut, so it must be the strict kind — a stale
18
+ * list (a fetch that failed) proves nothing, and the route must stop rather
19
+ * than write a proposal onto a branch it never confirmed.
20
+ */
21
+
22
+ const KB = 'knowledge-base';
23
+ const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
24
+ const MIA = { id: 'u-mia', email: 'mia@x.io', name: 'Mia' };
25
+ const FOLDER = 'Skills/Eng/deploy';
26
+
27
+ async function makeHarness(listBranches: IWorkflowService['listBranches']) {
28
+ const workflow = {
29
+ listChangeRequestsAuthoredBy: vi.fn(async () => []),
30
+ listBranches: vi.fn(listBranches),
31
+ createBranch: vi.fn(async () => ({ name: 'x', isDefault: false, isProtected: false })),
32
+ commitChanges: vi.fn(async () => null),
33
+ openChangeRequest: vi.fn(async () => ({ number: 42 })),
34
+ } as unknown as IWorkflowService;
35
+ const workspaceService = {
36
+ getOrCreateForBranch: vi.fn(async (branch: string) => ({ id: workspaceIdForBranch(branch) })),
37
+ readFile: vi.fn(async () => '---\n---\nwrite:\n - Eve <eve@x.io>\n'),
38
+ writeFile: vi.fn(async () => undefined),
39
+ } as unknown as WorkspaceService;
40
+ const accessControl = { canWrite: vi.fn(async () => false) } as unknown as IAccessControl;
41
+ const skillService = {
42
+ listSkills: vi.fn(async () => [{ name: 'deploy', description: '', path: FOLDER }]),
43
+ } as unknown as ISkillService;
44
+
45
+ const app = express();
46
+ app.use(express.json());
47
+ app.use(
48
+ '/api',
49
+ createSkillAccessRequestRoutes({
50
+ skillService,
51
+ accessControl,
52
+ workflow,
53
+ workspaceService,
54
+ joinRequests: { list: vi.fn(), reconcile: vi.fn() } as unknown as JoinRequestsService,
55
+ kbDirName: KB,
56
+ resolveUser: async () => MIA,
57
+ }),
58
+ );
59
+ const server = await new Promise<Server>((resolve) => {
60
+ const s = app.listen(0, () => resolve(s));
61
+ });
62
+ const { port } = server.address() as AddressInfo;
63
+ return { server, url: `http://127.0.0.1:${port}/api/skills/deploy/access-request`, workflow, workspaceService };
64
+ }
65
+
66
+ describe('POST /skills/:name/access-request — the branch probe', () => {
67
+ let server: Server | null = null;
68
+ afterEach(async () => {
69
+ // Released here, not after the assertions: a failing assertion must not
70
+ // leave the console muted for every test that follows.
71
+ vi.restoreAllMocks();
72
+ if (server) await new Promise<void>((r) => server!.close(() => r()));
73
+ server = null;
74
+ });
75
+
76
+ it('proves absence with a STRICT fresh listing, then cuts the branch and opens the request', async () => {
77
+ const h = await makeHarness(async () => []);
78
+ server = h.server;
79
+ const res = await fetch(h.url, { method: 'POST' });
80
+ expect(res.status).toBe(200);
81
+ expect(await res.json()).toEqual({ ok: true, number: 42 });
82
+ expect(h.workflow.listBranches).toHaveBeenCalledWith(wsId, { freshFetch: true, strictFetch: true });
83
+ expect(h.workflow.createBranch).toHaveBeenCalledWith(wsId, joinBranchFor(MIA.email, FOLDER), DEFAULT_BRANCH);
84
+ // The whole chain the probe exists to enable: the grant is written AND committed.
85
+ expect(h.workspaceService.writeFile).toHaveBeenCalledTimes(1);
86
+ expect(h.workflow.commitChanges).toHaveBeenCalledTimes(1);
87
+ });
88
+
89
+ it('a create that loses the race proceeds once the fresh re-probe shows the branch', async () => {
90
+ const branch = joinBranchFor(MIA.email, FOLDER);
91
+ // Absent on the first probe, present on the second: someone else cut it in between.
92
+ let probes = 0;
93
+ const h = await makeHarness(async () => (probes++ === 0 ? [] : [{ name: branch, isDefault: false, isProtected: false }]));
94
+ (h.workflow.createBranch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('already exists'));
95
+ server = h.server;
96
+ const res = await fetch(h.url, { method: 'POST' });
97
+ expect(res.status).toBe(200);
98
+ expect(await res.json()).toEqual({ ok: true, number: 42 });
99
+ expect(h.workflow.listBranches).toHaveBeenCalledTimes(2);
100
+ expect(h.workspaceService.writeFile).toHaveBeenCalledTimes(1);
101
+ expect(h.workflow.commitChanges).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it('a create that fails with no branch to show for it stops the request — nothing is written on a branch that was not made', async () => {
105
+ vi.spyOn(console, 'error').mockImplementation(() => {});
106
+ const h = await makeHarness(async () => []);
107
+ (h.workflow.createBranch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('origin refused'));
108
+ server = h.server;
109
+ const res = await fetch(h.url, { method: 'POST' });
110
+ expect(res.status).toBe(500);
111
+ expect(h.workflow.listBranches).toHaveBeenCalledTimes(2);
112
+ expect(h.workspaceService.writeFile).not.toHaveBeenCalled();
113
+ expect(h.workflow.openChangeRequest).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('a listing that could not prove anything stops the request before a branch or a byte is written', async () => {
117
+ vi.spyOn(console, 'error').mockImplementation(() => {});
118
+ const h = await makeHarness(async () => {
119
+ throw new Error('the fetch from origin failed, so the branch list cannot prove absence');
120
+ });
121
+ server = h.server;
122
+ const res = await fetch(h.url, { method: 'POST' });
123
+ expect(res.status).toBe(500);
124
+ expect(h.workflow.createBranch).not.toHaveBeenCalled();
125
+ expect(h.workspaceService.writeFile).not.toHaveBeenCalled();
126
+ expect(h.workflow.openChangeRequest).not.toHaveBeenCalled();
127
+ });
128
+ });
@@ -67,6 +67,62 @@ describe('SkillService', () => {
67
67
  });
68
68
  afterEach(() => rm(root, { recursive: true, force: true }));
69
69
 
70
+ test('lists shared skills under the Skills root alongside plugin skills', async () => {
71
+ const shared = join(root, wsId, KB_DIR, 'Skills', 'Engineering', 'deploy');
72
+ await mkdir(shared, { recursive: true });
73
+ await writeFile(join(shared, 'SKILL.md'), '---\ndescription: Ship it.\n---\n\n# Deploy\n');
74
+ // A skill directly under the root, no scope folder — also a skill.
75
+ const bare = join(root, wsId, KB_DIR, 'Skills', 'triage');
76
+ await mkdir(bare, { recursive: true });
77
+ await writeFile(join(bare, 'SKILL.md'), '---\ndescription: Triage.\n---\n');
78
+
79
+ const list = await svc().listSkills();
80
+ expect(list.find((s) => s.name === 'deploy')?.path).toBe('Skills/Engineering/deploy');
81
+ expect(list.find((s) => s.name === 'triage')?.path).toBe('Skills/triage');
82
+ // The plugin tree is still scanned: the union is the catalog.
83
+ expect(list.find((s) => s.name === 'rfi')?.path).toBe('Plugins/rfi');
84
+
85
+ const res = await svc().getSkill('u@x.io', 'deploy');
86
+ expect(res.ok && res.kind === 'skill' && res.skill.body).toContain('# Deploy');
87
+ });
88
+
89
+ test('honours a .bevelignore inside a root, so a build output never shadows the source', async () => {
90
+ // A repository that commits a compiled copy of every skill beside the
91
+ // source: without the ignore, `dist/…/rfi` would collide with `Plugins/rfi`.
92
+ const dist = join(root, wsId, KB_DIR, 'Skills', 'dist', 'rfi');
93
+ await mkdir(dist, { recursive: true });
94
+ await writeFile(join(dist, 'SKILL.md'), '---\ndescription: compiled copy\n---\n');
95
+ await writeFile(join(root, wsId, KB_DIR, 'Skills', '.bevelignore'), 'dist/\n');
96
+
97
+ const list = await svc().listSkills();
98
+ expect(list.filter((s) => s.name === 'rfi').map((s) => s.path)).toEqual(['Plugins/rfi']);
99
+ expect(list.some((s) => s.path.includes('/dist/'))).toBe(false);
100
+ });
101
+
102
+ test('ignores the repo-root .bevelignore: hiding a root from the file tree must not empty the catalog', async () => {
103
+ // The seeded template hides `Plugins/` (and `Skills/`) from the Knowledge
104
+ // tree with exactly this file. The catalog is what those roots exist for.
105
+ await writeFile(join(root, wsId, KB_DIR, '.bevelignore'), 'Plugins/\nSkills/\n');
106
+ const shared = join(root, wsId, KB_DIR, 'Skills', 'deploy');
107
+ await mkdir(shared, { recursive: true });
108
+ await writeFile(join(shared, 'SKILL.md'), '---\ndescription: Ship it.\n---\n');
109
+
110
+ const list = await svc().listSkills();
111
+ expect(list.map((s) => s.name).sort()).toEqual(['coding-guidelines', 'deploy', 'rfi']);
112
+ });
113
+
114
+ test('a nested .bevelignore applies beneath the folder that carries it', async () => {
115
+ const scope = join(root, wsId, KB_DIR, 'Skills', 'Sales');
116
+ await mkdir(join(scope, 'drafts', 'pitch'), { recursive: true });
117
+ await mkdir(join(scope, 'pitch'), { recursive: true });
118
+ await writeFile(join(scope, 'drafts', 'pitch', 'SKILL.md'), '---\ndescription: draft\n---\n');
119
+ await writeFile(join(scope, 'pitch', 'SKILL.md'), '---\ndescription: released\n---\n');
120
+ await writeFile(join(scope, '.bevelignore'), 'drafts/\n');
121
+
122
+ const list = await svc().listSkills();
123
+ expect(list.filter((s) => s.name === 'pitch').map((s) => s.path)).toEqual(['Skills/Sales/pitch']);
124
+ });
125
+
70
126
  test('lists skills with a block-scalar description parsed correctly', async () => {
71
127
  const skills = await svc().listSkills();
72
128
  expect(skills).toHaveLength(2);
@@ -145,6 +201,35 @@ describe('SkillService', () => {
145
201
  expect(trav).toEqual({ ok: false, error: 'invalid_file' });
146
202
  });
147
203
 
204
+ test('an ignored SKILL.md suppresses the skill and NEVER promotes its assets to skills', async () => {
205
+ const rfi = join(root, wsId, KB_DIR, 'Plugins', 'rfi');
206
+ await writeFile(join(rfi, '.bevelignore'), 'SKILL.md\n');
207
+ await mkdir(join(rfi, 'examples'), { recursive: true });
208
+ await writeFile(join(rfi, 'examples', 'SKILL.md'), '---\ndescription: An example, not a skill.\n---\n');
209
+ const names = (await svc().listSkills()).map((s) => s.name);
210
+ expect(names).not.toContain('rfi');
211
+ expect(names).not.toContain('examples');
212
+ });
213
+
214
+ test('ignore rules reach a skill\'s bundled files: hidden from the listing and refused when asked for', async () => {
215
+ const rfi = join(root, wsId, KB_DIR, 'Plugins', 'rfi');
216
+ await writeFile(join(rfi, '.bevelignore'), 'scripts/\n');
217
+ const res = await svc().getSkill('user@x.eu', 'rfi');
218
+ expect(res.ok && res.kind === 'skill' ? res.skill.files : null).toEqual([]);
219
+ expect(await svc().getSkill('user@x.eu', 'rfi', 'scripts/build_xlsx.py')).toEqual({
220
+ ok: false,
221
+ error: 'not_found',
222
+ });
223
+ });
224
+
225
+ test('a skill that exists inline AND under the shared root resolves to the shared copy', async () => {
226
+ const shared = join(root, wsId, KB_DIR, 'Skills', 'Ops', 'rfi');
227
+ await mkdir(shared, { recursive: true });
228
+ await writeFile(join(shared, 'SKILL.md'), '---\ndescription: The canonical one.\n---\n');
229
+ const rfi = (await svc().listSkills()).find((s) => s.name === 'rfi');
230
+ expect(rfi?.path).toBe('Skills/Ops/rfi');
231
+ });
232
+
148
233
  test('listSkills filters by canRead for a given user', async () => {
149
234
  const denyRfi: IAccessControl = {
150
235
  canRead: async () => false,
@@ -2,6 +2,7 @@ export { SkillService } from './skills.service.js';
2
2
  export { PendingSkillsService } from './pending-skills.service.js';
3
3
  export { registerSkillsTools } from './skills.tools.js';
4
4
  export { createSkillsRoutes } from './skills.routes.js';
5
+ export { createSkillAccessRequestRoutes } from './skill-access-requests.routes.js';
5
6
  export type {
6
7
  ISkillService,
7
8
  IPendingSkillService,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  DEFAULT_BRANCH,
3
3
  PLUGINS_DIR,
4
+ SKILLS_DIR,
4
5
  type ChangeRequest,
5
6
  type IWorkflowService,
6
7
  } from '@bevel-software/platform-shared';
@@ -118,6 +119,8 @@ export class PendingSkillsService implements IPendingSkillService {
118
119
  name: isSafeSkillName(declared) ? declared : folderName,
119
120
  description: fm.description,
120
121
  version: fm.version,
122
+ owner: fm.owner,
123
+ lifecycle: fm.lifecycle,
121
124
  path: folder,
122
125
  changeRequestNumber: cr.number,
123
126
  branch: cr.branch,
@@ -156,11 +159,10 @@ export class PendingSkillsService implements IPendingSkillService {
156
159
  */
157
160
  function isSkillDoc(repoRelPath: string): boolean {
158
161
  const segments = repoRelPath.split('/');
159
- return (
160
- segments.length >= 4 &&
161
- segments[0] === PLUGINS_DIR &&
162
- segments[segments.length - 1] === SKILL_DOC
163
- );
162
+ if (segments[segments.length - 1] !== SKILL_DOC) return false;
163
+ // Under `Skills/` a skill may sit directly below the root: `Skills/<skill>/SKILL.md`.
164
+ if (segments[0] === SKILLS_DIR) return segments.length >= 3;
165
+ return segments.length >= 4 && segments[0] === PLUGINS_DIR;
164
166
  }
165
167
 
166
168
  /** The skill folder holding a SKILL.md. */