@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,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import type { AuthProviderPlugin } from '../modules/auth/auth.routes.js';
2
3
  import type { AuthUser } from '@bevel-software/platform-shared';
3
4
  import {
@@ -5,7 +6,7 @@ import {
5
6
  configureBranchModel,
6
7
  validateBranchModel,
7
8
  PROTECTED_BRANCHES,
8
- PLUGINS_DIR,
9
+ configureKbLayout,
9
10
  } from '@bevel-software/platform-shared';
10
11
  import { CoreConfig } from '../core-config.js';
11
12
  import { getDb, type Database } from '../modules/database/connection.js';
@@ -15,10 +16,20 @@ import { WorkspaceService } from '../modules/workspace/workspace.service.js';
15
16
  import { RoutineWritePolicyService } from '../modules/workspace/routine-write-policy.js';
16
17
  import { KbStartupRunner } from '../modules/workspace/startup/kb-startup-runner.js';
17
18
  import { GroupsToPluginsStep } from '../modules/workspace/startup/steps/groups-to-plugins.step.js';
19
+ import { PluginManifestsStep } from '../modules/workspace/startup/steps/plugin-manifests.step.js';
18
20
  import { TemplateFilesStep } from '../modules/workspace/startup/steps/template-files.step.js';
19
21
  import { RolesYamlStep } from '../modules/workspace/startup/steps/roles-yaml.step.js';
20
22
  import { buildSeedTree } from '../modules/workspace/startup/steps/seed-tree.js';
21
23
  import { DeploymentSettingsService } from '../modules/settings/deployment-settings.service.js';
24
+ import { KbSyncService } from '../modules/kb-sync/kb-sync.service.js';
25
+
26
+ /** The hosted MCP endpoint at a deployment address, with any userinfo stripped. */
27
+ function mcpEndpointUrl(publicBackendUrl: string): string {
28
+ const url = new URL('/api/mcp', publicBackendUrl);
29
+ url.username = '';
30
+ url.password = '';
31
+ return url.toString();
32
+ }
22
33
 
23
34
  /** `a.com, b.com` → `['a.com','b.com']`, tolerating a leading `@` or `.`. */
24
35
  function parseDomainList(raw: string): string[] {
@@ -40,7 +51,26 @@ import { GroupsAdminService } from '../modules/access/groups-admin.service.js';
40
51
  import { PendingSkillsService, SkillService } from '../modules/skills/index.js';
41
52
  import { ToolManualService } from '../modules/tool-manuals/index.js';
42
53
  import { McpServerEditService } from '../modules/tool-manuals/mcp-server-edit.service.js';
43
- import { PluginIndexService, PluginProvisionService, JoinRequestsService } from '../modules/plugins/index.js';
54
+ import {
55
+ PluginIndexService,
56
+ PluginProvisionService,
57
+ JoinRequestsService,
58
+ PluginLinkIndex,
59
+ PluginLinksService,
60
+ PluginRenameService,
61
+ MarketplaceCompilerService,
62
+ KbPluginSource,
63
+ } from '../modules/plugins/index.js';
64
+ import { MarketplaceRepoService } from '../modules/marketplace/index.js';
65
+ import {
66
+ GitHubFacadeCredentialsService,
67
+ GitHubFacade,
68
+ DbGitHubFacadeCodeStore,
69
+ DbGitHubFacadeCredentialsStore,
70
+ CLAUDE_CONSUMER,
71
+ GITHUB_LINK_KEY_KIND,
72
+ GITHUB_LINK_KEY_PREFIX,
73
+ } from '../modules/marketplace/github-facade/index.js';
44
74
  import {
45
75
  DbSecretsVaultService,
46
76
  McpOAuthDiscoveryService,
@@ -94,6 +124,7 @@ import { TokenCrypto } from '../shared/token-crypto.js';
94
124
  import { UpdateCheckService } from '../modules/update-check/update-check.service.js';
95
125
  import { resolveAppVersion } from '../version.js';
96
126
  import { noopRecoveryAgent, type CorePorts } from './core-ports.js';
127
+ import { registerCatalogCacheInvalidation } from './catalog-cache-invalidation.js';
97
128
 
98
129
  /**
99
130
  * Everything the CORE composition builds: the core services `createCoreServer`
@@ -128,6 +159,18 @@ export interface CoreServices {
128
159
  pluginIndexService: PluginIndexService;
129
160
  pluginProvisionService: PluginProvisionService;
130
161
  joinRequestsService: JoinRequestsService;
162
+ /** Which plugins hold which skills (inline or linked) — see `PluginLinkIndex`. */
163
+ pluginLinkIndex: PluginLinkIndex;
164
+ /** Link / unlink / repair shared skills into plugins. */
165
+ pluginLinksService: PluginLinksService;
166
+ pluginRenameService: PluginRenameService;
167
+ /** Source layout → distribution layout, for one audience. */
168
+ marketplaceCompiler: MarketplaceCompilerService;
169
+ /** The bare repository the per-user marketplace git endpoint serves from. */
170
+ marketplaceRepo: MarketplaceRepoService;
171
+ /** The GitHub Enterprise facade products such as claude.ai add the marketplace through. */
172
+ githubFacade: GitHubFacade;
173
+ githubFacadeCredentials: GitHubFacadeCredentialsService;
131
174
  authService: AuthService;
132
175
  authMiddleware: ReturnType<typeof createAuthMiddleware>;
133
176
  accountErasureService: AccountErasureService;
@@ -137,6 +180,8 @@ export interface CoreServices {
137
180
  reviewWorkflowService: ReviewWorkflowService;
138
181
  fileLockService: FileLockService;
139
182
  workflowService: WorkflowService;
183
+ /** Remote sync (`POST /api/sync`): a git host or pipeline makes the clones catch up. */
184
+ kbSyncService: KbSyncService;
140
185
  eventBus: WorkflowEventBus;
141
186
  fileChangeNotifier: FileChangeNotifier;
142
187
  pendingCommitsService: PendingCommitsService;
@@ -242,6 +287,11 @@ export async function createCoreServices(
242
287
  protectedBranches: settings.resolve('protectedBranches'),
243
288
  };
244
289
  if (!validateBranchModel(branchModel)) configureBranchModel(branchModel);
290
+ // The KB layout — the three renameable roots — applied the same way and at
291
+ // the same moment, before any service captures a root name. Unlike the
292
+ // branch model it always resolves (every root has a default), so an invalid
293
+ // value is a real misconfiguration and stops the boot.
294
+ configureKbLayout(settings.resolveKbLayout());
245
295
  // A token supplied through the setup screen has to reach the credential
246
296
  // helper, which reads `$GITHUB_TOKEN` at call time.
247
297
  settings.syncGitTokenEnv();
@@ -269,6 +319,7 @@ export async function createCoreServices(
269
319
  // whatever the distribution appends.
270
320
  const kbStartupSteps = [
271
321
  new GroupsToPluginsStep(),
322
+ new PluginManifestsStep(),
272
323
  new TemplateFilesStep(extraDirs),
273
324
  new RolesYamlStep([config.adminEmail]),
274
325
  ...(ports.kbStartupSteps ?? []),
@@ -324,17 +375,27 @@ export async function createCoreServices(
324
375
  // Tool manuals: user-authored `*.tool` files under `Plugins/` in the default
325
376
  // branch — access-controlled like Skills, served to external agents via
326
377
  // `GET /api/agent/all-tools` and registered on the MCP proxy's UTCP client.
327
- const toolManualService = new ToolManualService(workspaceService, accessControl, kbDirName);
378
+ // Where plugins come from: one walk of the plugins root that reads every
379
+ // plugin folder in whichever file shape it carries (see
380
+ // modules/plugins/discovery). Nothing to configure, nothing to document.
381
+ const pluginSource = new KbPluginSource();
382
+ const toolManualService = new ToolManualService(workspaceService, accessControl, kbDirName, Date.now, pluginSource);
328
383
  // Plugins: the folders under `Plugins/` that carry a
329
384
  // team's skills AND the tools they need. Enumerated for EVERY authenticated
330
385
  // caller — a plugin they cannot read still exists for them, as a locked one —
331
386
  // with the counts read off the two catalogs above rather than a second scan.
387
+ // The link index resolves manifests against the released catalog, and the
388
+ // plugin index counts through it (inline + linked), so it comes first.
389
+ const pluginLinkIndex = new PluginLinkIndex(workspaceService, skillService, accessControl, kbDirName, Date.now, pluginSource);
332
390
  const pluginIndexService = new PluginIndexService(
333
391
  workspaceService,
334
392
  accessControl,
335
393
  skillService,
336
394
  toolManualService,
337
395
  kbDirName,
396
+ Date.now,
397
+ pluginLinkIndex,
398
+ pluginSource,
338
399
  );
339
400
  // Auth service — resolves identities for login, PR author attribution, and
340
401
  // access lookups. (Change requests now store the author email directly, so
@@ -450,6 +511,62 @@ export async function createCoreServices(
450
511
  // only needs to read files at refs and to close a request whose proposals
451
512
  // have all landed.
452
513
  const joinRequestsService = new JoinRequestsService(workspaceService, workflowService);
514
+ // Links land as ordinary default-branch commits and change what the plugin
515
+ // index counts, so a link drops that cache too.
516
+ const pluginLinksService = new PluginLinksService(
517
+ workspaceService,
518
+ workflowService,
519
+ accessControl,
520
+ skillService,
521
+ pluginLinkIndex,
522
+ kbDirName,
523
+ eventBus,
524
+ () => pluginIndexService.invalidate(),
525
+ );
526
+ // A rename rewrites grants across the knowledge base in one batch commit,
527
+ // then drops every cache that keyed on the old identity.
528
+ const pluginRenameService = new PluginRenameService(
529
+ workspaceService,
530
+ workflowService,
531
+ accessControl,
532
+ pluginSource,
533
+ kbDirName,
534
+ eventBus,
535
+ () => {
536
+ pluginIndexService.invalidate();
537
+ pluginLinkIndex.invalidate();
538
+ },
539
+ );
540
+ // Source → distribution. The marketplace's identity is fixed for now; the
541
+ // per-user git endpoint and any mirror both compile through this.
542
+ const marketplaceCompiler = new MarketplaceCompilerService(
543
+ workspaceService,
544
+ accessControl,
545
+ skillService,
546
+ pluginLinkIndex,
547
+ kbDirName,
548
+ {
549
+ name: 'hexis',
550
+ owner: 'Hexis',
551
+ description: 'Skills and plugins from this knowledge base',
552
+ // The knowledge base as a tool, shipped in the skills plugin so an
553
+ // agent installing from the marketplace can read what its skills refer
554
+ // to. The same address `/api/config` and the OAuth metadata publish,
555
+ // userinfo stripped; the client's OAuth flow supplies the identity.
556
+ knowledgeBaseMcp: { name: 'hexis', url: mcpEndpointUrl(config.publicBackendUrl) },
557
+ },
558
+ pluginSource,
559
+ );
560
+ // Sibling of the workspaces root, like the spill store: one bare repo, one
561
+ // git namespace per caller (see marketplace-repo.service.ts).
562
+ const marketplaceRepo = new MarketplaceRepoService(
563
+ path.resolve(config.workspacesRoot, '..', 'marketplace.git'),
564
+ marketplaceCompiler,
565
+ );
566
+
567
+ // Remote sync. Drives the workflow module's per-branch pull-and-announce
568
+ // over every clone the workspace service knows about; owns no git of its own.
569
+ const kbSyncService = new KbSyncService(workflowService, workspaceService);
453
570
  // Server-scoped MCP editing — the tool page's edit form. One server's truth
454
571
  // spans a plugin's mcp.json AND plugin.json extensions block, and this is
455
572
  // the ONE writer that rewrites both entries and commits them together (see
@@ -486,43 +603,16 @@ export async function createCoreServices(
486
603
  workflowService,
487
604
  );
488
605
 
489
- // Subscriber A — catalog freshness: a committed change drops the affected
490
- // caches immediately instead of waiting out their TTLs. The tool/skill
491
- // catalogs are global but read the DEFAULT branch only, so only
492
- // default-branch changes under their folders matter. (The kb-graph id-index
493
- // invalidation that used to live in this subscriber is registered by the
494
- // enterprise overlay, next to the kb-graph service it belongs to — the
495
- // caches are independent, so the split preserves behavior.)
496
- fileChangeNotifier.onFilesChanged(({ branch, paths }) => {
497
- if (branch !== DEFAULT_BRANCH) return;
498
- // Skills, tools and the plugin index all live under `Plugins/`, so one
499
- // touch check drives all three caches. An access grant lands as a
500
- // default-branch change to `Plugins/<plugin>/access.md`, so this is also
501
- // what makes a newly-granted plugin unlock within one round-trip instead
502
- // of one TTL.
503
- const touched = paths.some((p) => p.startsWith(`${kbDirName}/${PLUGINS_DIR}/`));
504
- if (touched) {
505
- toolManualService.invalidate();
506
- skillService.invalidate();
507
- pluginIndexService.invalidate();
508
- }
509
-
510
- });
511
-
512
- // Subscriber B — WRITE-time freshness for the same three caches. The
513
- // workspace routes emit `fs-tree-changed` the moment bytes hit a working
514
- // tree; Subscriber A above fires only when the ASYNC commit lands. Between
515
- // the two, "create a skill, reload the catalog" raced the commit pipeline
516
- // and lost — the new skill's card stayed invisible until a refresh outlived
517
- // the TTL. The catalogs scan the working tree anyway, so invalidating at
518
- // write time makes the very next read see the file. No path filter: this
519
- // event carries none, and a spurious drop only costs one re-scan.
520
- eventBus.onEmit((event) => {
521
- if (event.kind !== 'fs-tree-changed') return;
522
- if (!('branch' in event) || event.branch !== DEFAULT_BRANCH) return;
523
- toolManualService.invalidate();
524
- skillService.invalidate();
525
- pluginIndexService.invalidate();
606
+ // Catalog freshness: the skill / tool-manual / plugin-index caches all scan
607
+ // the DEFAULT branch's working tree, and all three go stale on the same
608
+ // events (a commit, a working-tree write, a merge). Wired in one place so a
609
+ // new way of reaching the default branch cannot refresh two of them and
610
+ // leave the third serving last minute's answer.
611
+ registerCatalogCacheInvalidation({
612
+ eventBus,
613
+ fileChangeNotifier,
614
+ kbDirName,
615
+ catalogs: [toolManualService, skillService, pluginIndexService, pluginLinkIndex],
526
616
  });
527
617
 
528
618
  // Admin = `Admin` role in roles.yaml, resolved through the access model on the
@@ -609,7 +699,30 @@ export async function createCoreServices(
609
699
  // GENERIC proxy: per session it discovers the UTCP manual at /api/agent/utcp
610
700
  // over loopback and re-exposes every tool, dispatching calls back through the
611
701
  // REST tool surface (so agent logic + metering live there, once).
612
- const externalApiKeyService = new ExternalApiKeyService(db, config.externalApiKeyPrefix);
702
+ // Connection keys also come as GitHub-shaped links (`gho_…`, kind
703
+ // `github-link`): the same key, minted by the marketplace facade below when
704
+ // a person connects an account on claude.ai, told apart by its stored kind.
705
+ const externalApiKeyService = new ExternalApiKeyService(db, config.externalApiKeyPrefix, {
706
+ [GITHUB_LINK_KEY_KIND]: GITHUB_LINK_KEY_PREFIX,
707
+ });
708
+
709
+ // The facade that lets products which sync marketplaces only from a GitHub
710
+ // Enterprise Server (claude.ai, Cowork) add the per-user marketplace:
711
+ // generated app credentials an Owner registers once, and the connect flow
712
+ // that turns a hexis session into a connection key the product holds.
713
+ // Reuses the MCP flow's signed state and /connect page — see
714
+ // modules/marketplace/github-facade.
715
+ const githubFacadeCredentials = new GitHubFacadeCredentialsService(
716
+ new DbGitHubFacadeCredentialsStore(db, config.secretsEncKey ? new TokenCrypto(config.secretsEncKey) : null),
717
+ );
718
+ const githubFacade = new GitHubFacade({
719
+ credentials: githubFacadeCredentials,
720
+ codes: new DbGitHubFacadeCodeStore(db),
721
+ keys: externalApiKeyService,
722
+ consumers: [CLAUDE_CONSUMER],
723
+ stateSecret: config.jwtSecret,
724
+ publicFrontendUrl: config.publicFrontendUrl,
725
+ });
613
726
  const mcpSessionStore = new McpSessionStore();
614
727
  const mcpService = new McpService(
615
728
  mcpSessionStore,
@@ -826,6 +939,13 @@ export async function createCoreServices(
826
939
  pluginIndexService,
827
940
  pluginProvisionService,
828
941
  joinRequestsService,
942
+ pluginLinkIndex,
943
+ pluginLinksService,
944
+ pluginRenameService,
945
+ marketplaceCompiler,
946
+ marketplaceRepo,
947
+ githubFacade,
948
+ githubFacadeCredentials,
829
949
  authService,
830
950
  authMiddleware,
831
951
  accountErasureService,
@@ -835,6 +955,7 @@ export async function createCoreServices(
835
955
  reviewWorkflowService,
836
956
  fileLockService,
837
957
  workflowService,
958
+ kbSyncService,
838
959
  eventBus,
839
960
  fileChangeNotifier,
840
961
  pendingCommitsService,
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+
6
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
7
+ import { AccessControlService } from '../access-control.service.js';
8
+
9
+ const KB_DIR = 'knowledge-base';
10
+
11
+ async function writeFile(repo: string, rel: string, contents: string): Promise<void> {
12
+ const abs = path.join(repo, rel);
13
+ await fs.mkdir(path.dirname(abs), { recursive: true });
14
+ await fs.writeFile(abs, contents);
15
+ }
16
+
17
+ /**
18
+ * `invalidate()` fires exactly when the tree changes under a model load that
19
+ * is still reading it: a marketplace compile for one caller drops the caches
20
+ * while a compile for another caller is mid-load, or a commit lands while the
21
+ * explorer builds. The load that started on the OLD tree must not store its
22
+ * model after the drop — otherwise every read for the next TTL resolves
23
+ * against a tree that is already gone.
24
+ */
25
+ describe('AccessControlService — a load that straddles invalidate() never repopulates the cache', () => {
26
+ let root: string;
27
+ const workspaceId = 'ws-invalidate-race';
28
+
29
+ beforeEach(async () => {
30
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-invalidate-race-'));
31
+ });
32
+ afterEach(async () => {
33
+ vi.restoreAllMocks();
34
+ await fs.rm(root, { recursive: true, force: true });
35
+ });
36
+
37
+ it('a read that starts after the drop keeps seeing the new tree once the old load lands', async () => {
38
+ const workspaceDir = path.join(root, workspaceId);
39
+ const repo = path.join(workspaceDir, KB_DIR);
40
+ await writeFile(repo, 'roles.yaml', 'roles:\n Admin:\n - admin@x.io\n');
41
+ await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
42
+ // Notes is closed to zoe on the old tree.
43
+ await writeFile(repo, 'Notes/access.md', '---\nread:\n - Admin\n---\n');
44
+ await writeFile(repo, 'Notes/plan.md', '# plan\n');
45
+ const service = new AccessControlService(
46
+ { getWorkspacePath: async () => workspaceDir } as unknown as WorkspaceService,
47
+ KB_DIR,
48
+ );
49
+
50
+ // Hold the OLD load exactly where the walk reads Notes/access.md: the
51
+ // bytes are read now (the closed rules), but the load cannot finish until
52
+ // released — which is what a slow disk or a large tree does for free.
53
+ const realReadFile = fs.readFile.bind(fs);
54
+ let release: () => void = () => undefined;
55
+ const held = new Promise<void>((resolve) => (release = resolve));
56
+ let reachedHold: () => void = () => undefined;
57
+ const reached = new Promise<void>((resolve) => (reachedHold = resolve));
58
+ let holdOnce = true;
59
+ vi.spyOn(fs, 'readFile').mockImplementation(async (file, ...rest) => {
60
+ const result = await (realReadFile as (...a: unknown[]) => Promise<unknown>)(file, ...rest);
61
+ if (holdOnce && String(file).endsWith(path.join('Notes', 'access.md'))) {
62
+ holdOnce = false;
63
+ reachedHold();
64
+ await held;
65
+ }
66
+ return result as never;
67
+ });
68
+ const oldLoad = service.canRead(workspaceId, 'zoe@x.io', 'Notes/plan.md');
69
+ await reached;
70
+
71
+ // The tree moves and the caches are dropped while that load is in flight.
72
+ await writeFile(repo, 'Notes/access.md', '---\nread:\n - Admin\n - Zoe <zoe@x.io>\n---\n');
73
+ service.invalidate(workspaceId);
74
+ try {
75
+ await expect(service.canRead(workspaceId, 'zoe@x.io', 'Notes/plan.md')).resolves.toBe(true);
76
+ } finally {
77
+ // The old load lands (also on a failed assertion, so the test cannot hang).
78
+ release();
79
+ }
80
+
81
+ // It answers for the tree it read — closed — but must not become the
82
+ // cached model for the reads that follow.
83
+ await expect(oldLoad).resolves.toBe(false);
84
+ await expect(service.canRead(workspaceId, 'zoe@x.io', 'Notes/plan.md')).resolves.toBe(true);
85
+ });
86
+ });
@@ -953,7 +953,7 @@ describe('AccessControlService', () => {
953
953
 
954
954
  const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR);
955
955
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
956
- expect(e).toEqual({ restricted: true, principals: [], roles: [], users: [] });
956
+ expect(e).toEqual({ restricted: true, principals: [], roles: [], users: [], publicVia: [] });
957
957
  });
958
958
 
959
959
  it('reports restricted=false when read: everyone applies', async () => {
@@ -963,7 +963,17 @@ describe('AccessControlService', () => {
963
963
 
964
964
  const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR);
965
965
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
966
- expect(e).toEqual({ restricted: false, principals: [], roles: [], users: [] });
966
+ // The flag says public; the lists still name the grants (write folds into read).
967
+ expect(e).toEqual({
968
+ restricted: false,
969
+ principals: [
970
+ { name: 'Admin', kind: 'role' },
971
+ { name: 'everyone', kind: 'role' },
972
+ ],
973
+ roles: ['Admin', 'everyone'],
974
+ users: [],
975
+ publicVia: [], // public by a literal line, through no plugin
976
+ });
967
977
  });
968
978
 
969
979
  it('restricted=false when a closer everyone grant shadows a farther by-name deny', async () => {
@@ -978,7 +988,13 @@ describe('AccessControlService', () => {
978
988
  // node really is readable by everyone — not restricted.
979
989
  expect(await svc.canRead(workspaceId, 'felix@example.com', 'Knowledge/Open/Foo.md')).toBe(true);
980
990
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Open/Foo.md');
981
- expect(e).toEqual({ restricted: false, principals: [], roles: [], users: [] });
991
+ expect(e).toEqual({
992
+ restricted: false,
993
+ principals: [{ name: 'everyone', kind: 'role' }],
994
+ roles: ['everyone'],
995
+ users: [],
996
+ publicVia: [],
997
+ });
982
998
  });
983
999
 
984
1000
  it('restricted=true when a same-scope by-name deny carves someone out of read: everyone', async () => {