@bevel-software/platform-core-backend 0.14.0 → 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 (385) 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 +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -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. */
@@ -0,0 +1,199 @@
1
+ import express from 'express';
2
+ import '../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
3
+ import {
4
+ DEFAULT_BRANCH,
5
+ joinBranchFor,
6
+ type AuthUser,
7
+ type IWorkflowService,
8
+ } from '@bevel-software/platform-shared';
9
+ import type { IAccessControl } from '../access/access-control.interface.js';
10
+ import { spliceGrant } from '../access-model/access-splice.js';
11
+ import { WorkflowDomainError } from '../../shared/domain-errors.js';
12
+ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
13
+ import type { WorkspaceService } from '../workspace/workspace.service.js';
14
+ import type { JoinRequestsService } from '../plugins/join-requests.service.js';
15
+ import type { ISkillService } from './skills.contract.js';
16
+
17
+ /**
18
+ * Asking for WRITE on a shared skill — the same machinery as asking to join a
19
+ * plugin, pointed at a skill folder instead of a plugin folder.
20
+ *
21
+ * POST /api/skills/:name/access-request → { ok, number }
22
+ * GET /api/skills/:name/access-requests → { requests } (skill editors)
23
+ * POST /api/skills/:name/access-requests/:n/reconcile → { closed }
24
+ *
25
+ * Why it exists: linking a skill into a plugin needs write on the skill's
26
+ * rules (the link grants the plugin's principal there), and a plugin manager
27
+ * often has no say over a skill some other scope owns. The request is a plain
28
+ * change request whose branch splices `write: <requester>` into the skill
29
+ * folder's `access.md`; the skill's editors see its proposals and accept
30
+ * through the ordinary grant path, and it retires itself once the default
31
+ * branch carries the grant (`JoinRequestsService`, keyed on the skill FOLDER
32
+ * path rather than a plugin name — the branch-name convention takes any
33
+ * exact string).
34
+ *
35
+ * Fail-closed like the skill catalog: a skill the caller cannot read answers
36
+ * as unknown, and the editors' listing answers `[]` to anyone else.
37
+ */
38
+ export function createSkillAccessRequestRoutes(deps: {
39
+ skillService: ISkillService;
40
+ accessControl: IAccessControl;
41
+ workflow: IWorkflowService;
42
+ workspaceService: WorkspaceService;
43
+ joinRequests: JoinRequestsService;
44
+ kbDirName: string;
45
+ resolveUser: (req: express.Request) => Promise<AuthUser | null>;
46
+ }): express.Router {
47
+ const { skillService, accessControl, workflow, workspaceService, joinRequests, kbDirName, resolveUser } = deps;
48
+ const router = express.Router();
49
+ const wsId = () => workspaceIdForBranch(DEFAULT_BRANCH);
50
+ const rulesOf = (folder: string) => `${folder}/access.md`;
51
+
52
+ /** The skill the caller may read, by name — or null after answering 404. */
53
+ async function readableSkill(
54
+ req: express.Request,
55
+ res: express.Response,
56
+ ): Promise<{ user: AuthUser; folder: string; name: string } | null> {
57
+ const user = await resolveUser(req);
58
+ if (!user) {
59
+ res.status(401).json({ error: 'Unauthenticated' });
60
+ return null;
61
+ }
62
+ const name = String(req.params.name);
63
+ const skill = (await skillService.listSkills(user.email)).find((s) => s.name === name);
64
+ if (!skill) {
65
+ res.status(404).json({ error: 'Unknown skill', kind: 'unknown-skill' });
66
+ return null;
67
+ }
68
+ return { user, folder: skill.path, name };
69
+ }
70
+
71
+ router.post('/skills/:name/access-request', async (req, res) => {
72
+ try {
73
+ const ctx = await readableSkill(req, res);
74
+ if (!ctx) return;
75
+ const { user, folder, name } = ctx;
76
+ if (await accessControl.canWrite(wsId(), user.email, rulesOf(folder))) {
77
+ res.status(409).json({ error: 'You can already edit this skill', kind: 'already-writable' });
78
+ return;
79
+ }
80
+ const branch = joinBranchFor(user.email, folder);
81
+ // FRESH: a repeated click must find the request the previous one opened,
82
+ // and the cached listing can trail it — a miss here would send the
83
+ // retry into a duplicate refusal.
84
+ const existing = (await workflow.listChangeRequestsAuthoredBy(user.email, { fresh: true })).find(
85
+ (cr) => cr.state === 'open' && cr.branch === branch,
86
+ );
87
+ if (existing) {
88
+ res.json({ ok: true, number: existing.number });
89
+ return;
90
+ }
91
+ // The branch may already exist (a request listed as closed, a retry
92
+ // after a failed open). Existence is PROBED, before and — if creation
93
+ // fails — after: the race shows up as "already exists" locally or as a
94
+ // rejected push when origin got there first, and a message is not a
95
+ // contract. A branch that is there is proceeded against; anything else
96
+ // is a real failure, and a proposal on a branch that was not made would
97
+ // be worse than the error. STRICT: the list proves absence, and a
98
+ // listing that could not fetch proves nothing — it throws instead of
99
+ // serving refs from before the branch was made (or deleted).
100
+ const branchExists = async () =>
101
+ (await workflow.listBranches(wsId(), { freshFetch: true, strictFetch: true })).some(
102
+ (b) => b.name === branch,
103
+ );
104
+ if (!(await branchExists())) {
105
+ try {
106
+ await workflow.createBranch(wsId(), branch, DEFAULT_BRANCH);
107
+ } catch (err) {
108
+ if (!(await branchExists())) throw err;
109
+ }
110
+ }
111
+ const ws = await workspaceService.getOrCreateForBranch(branch);
112
+ const accessPath = `${kbDirName}/${rulesOf(folder)}`;
113
+ // Only a PROVEN absence reads as "no rules yet": any other failure must
114
+ // not be turned into an empty file that a splice then overwrites.
115
+ const current = await workspaceService.readFile(ws.id, accessPath).catch((err: unknown) => {
116
+ const code = (err as { code?: unknown } | null)?.code;
117
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') throw err;
118
+ return '';
119
+ });
120
+ const spliced = spliceGrant(
121
+ current,
122
+ 'write',
123
+ { kind: 'user', email: user.email, displayName: user.name },
124
+ { target: 'folder' },
125
+ );
126
+ if (spliced.changed) {
127
+ await workspaceService.writeFile(ws.id, accessPath, spliced.text);
128
+ await workflow.commitChanges(ws.id, user, `Request write access to ${name}`);
129
+ }
130
+ const detail = await workflow.openChangeRequest(ws.id, user, {
131
+ sourceBranch: branch,
132
+ targetBranch: DEFAULT_BRANCH,
133
+ title: `Access request: ${name}`,
134
+ description:
135
+ `${user.name} asked to edit the skill ${name}. An editor of the skill accepts by ` +
136
+ `granting the access this branch proposes; the request closes itself once ` +
137
+ `every proposal has landed.`,
138
+ });
139
+ res.json({ ok: true, number: detail.number });
140
+ } catch (err) {
141
+ if (err instanceof WorkflowDomainError) {
142
+ res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
143
+ return;
144
+ }
145
+ console.error('[skills] failed to open an access request:', err);
146
+ res.status(500).json({ error: 'Failed to request access' });
147
+ }
148
+ });
149
+
150
+ /** The caller as an EDITOR of the skill's rules, or null after answering. */
151
+ async function requireEditor(
152
+ req: express.Request,
153
+ res: express.Response,
154
+ onDenied: () => void,
155
+ ): Promise<{ user: AuthUser; folder: string } | null> {
156
+ const ctx = await readableSkill(req, res);
157
+ if (!ctx) return null;
158
+ if (!(await accessControl.canWrite(wsId(), ctx.user.email, rulesOf(ctx.folder)))) {
159
+ onDenied();
160
+ return null;
161
+ }
162
+ return ctx;
163
+ }
164
+
165
+ router.get('/skills/:name/access-requests', async (req, res) => {
166
+ try {
167
+ const ctx = await requireEditor(req, res, () => res.json({ requests: [] }));
168
+ if (!ctx) return;
169
+ const crs = await workflow.listChangeRequests();
170
+ res.json({ requests: await joinRequests.list(ctx.folder, ctx.folder, crs, ctx.user) });
171
+ } catch (err) {
172
+ console.error('[skills] failed to list access requests:', err);
173
+ res.status(500).json({ error: 'Failed to list access requests' });
174
+ }
175
+ });
176
+
177
+ router.post('/skills/:name/access-requests/:number/reconcile', async (req, res) => {
178
+ try {
179
+ const ctx = await requireEditor(req, res, () => res.status(404).json({ error: 'Not found' }));
180
+ if (!ctx) return;
181
+ const crNumber = Number(req.params.number);
182
+ if (!Number.isSafeInteger(crNumber) || crNumber <= 0) {
183
+ res.status(400).json({ error: 'Invalid change request number' });
184
+ return;
185
+ }
186
+ const cr = await workflow.getChangeRequest(crNumber);
187
+ if (!cr) {
188
+ res.status(404).json({ error: 'Not found' });
189
+ return;
190
+ }
191
+ res.json({ closed: await joinRequests.reconcile(ctx.folder, ctx.folder, cr, ctx.user) });
192
+ } catch (err) {
193
+ console.error('[skills] failed to reconcile an access request:', err);
194
+ res.status(500).json({ error: 'Failed to reconcile the request' });
195
+ }
196
+ });
197
+
198
+ return router;
199
+ }