@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
@@ -0,0 +1,368 @@
1
+ import { describe, it, expect, beforeEach, afterEach } 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
+ import { parseAccessEntry } from '../../access-model/access-grammar.js';
9
+
10
+ /**
11
+ * `plugin/<Name>/<verb>` as a grantable principal: derived from the plugin
12
+ * folder's own access.md, resolved like any group, and usable on ANY path —
13
+ * a shared skill under `Skills/`, a Knowledge folder, anything. These tests
14
+ * drive the public resolver (`canRead`/`canWrite`, `eligibleReaders`,
15
+ * `kbPrincipals`) over a real on-disk tree; the synthesis is not tested
16
+ * through its own helper.
17
+ */
18
+
19
+ const KB_DIR = 'knowledge-base';
20
+
21
+ function stubWorkspaceService(workspaceId: string, workspaceDir: string): WorkspaceService {
22
+ return {
23
+ getWorkspacePath: async (id: string) => {
24
+ if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
25
+ return workspaceDir;
26
+ },
27
+ ensureRemotesFetched: async () => undefined,
28
+ } as unknown as WorkspaceService;
29
+ }
30
+
31
+ const ROLES_YAML = `roles:
32
+ Admin:
33
+ - admin@x.io
34
+ Engineer:
35
+ - eng@x.io
36
+ `;
37
+
38
+ const GROUPS_YAML = `groups:
39
+ Sales Team:
40
+ - sam@x.io
41
+ - sue@x.io
42
+ `;
43
+
44
+ /** A plugin folder's access.md in the body-governed format. */
45
+ function pluginAccessMd(body: string): string {
46
+ return `---\nread:\n - everyone\n---\n${body}`;
47
+ }
48
+
49
+ describe('plugin principals', () => {
50
+ let root: string;
51
+ const workspaceId = 'ws-plugins-1';
52
+
53
+ beforeEach(async () => {
54
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-plugin-principals-'));
55
+ });
56
+ afterEach(async () => {
57
+ await fs.rm(root, { recursive: true, force: true });
58
+ });
59
+
60
+ async function makeService(files: Record<string, string>) {
61
+ const workspaceDir = path.join(root, workspaceId);
62
+ const repo = path.join(workspaceDir, KB_DIR);
63
+ for (const [rel, contents] of Object.entries(files)) {
64
+ const abs = path.join(repo, rel);
65
+ await fs.mkdir(path.dirname(abs), { recursive: true });
66
+ await fs.writeFile(abs, contents);
67
+ }
68
+ return new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), KB_DIR);
69
+ }
70
+
71
+ // A plugin IS a folder with a manifest; its access.md is its roster.
72
+ const BASE = {
73
+ 'roles.yaml': ROLES_YAML,
74
+ 'groups.yaml': GROUPS_YAML,
75
+ 'access.md': '---\nwrite:\n - Admin\n---\n',
76
+ 'Plugins/GTM/plugin.json': '{"name":"gtm"}',
77
+ 'Plugins/GTM/access.md': pluginAccessMd(
78
+ 'read:\n - Sales Team\n - Ali <ali@x.io>\nwrite:\n - Engineer\nowner:\n - Owen <owen@x.io>\n',
79
+ ),
80
+ };
81
+
82
+ describe('grammar', () => {
83
+ it('canonicalises the name half to the manifest slug and keeps the verb', () => {
84
+ const parsed = parseAccessEntry('plugin/Sales Team/read');
85
+ expect(parsed.ok && parsed.entry.kind === 'role' && parsed.entry.role).toBe('plugin/sales-team/read');
86
+ const upper = parseAccessEntry('Plugin/GTM/Write');
87
+ expect(upper.ok && upper.entry.kind === 'role' && upper.entry.role).toBe('plugin/gtm/write');
88
+ const denied = parseAccessEntry('deny plugin/GTM/owner');
89
+ expect(denied.ok && denied.entry.kind === 'role' && denied.entry.deny).toBe(true);
90
+ });
91
+
92
+ it('rejects a token with a missing or unknown verb, naming the valid shapes', () => {
93
+ for (const bad of ['plugin/GTM', 'plugin/GTM/', 'plugin/GTM/admin', 'plugin//read']) {
94
+ const parsed = parseAccessEntry(bad);
95
+ expect(parsed.ok, bad).toBe(false);
96
+ expect(!parsed.ok && parsed.error).toMatch(/plugin\/<plugin>\/read/);
97
+ }
98
+ });
99
+ });
100
+
101
+ describe('resolution', () => {
102
+ it('grants a shared skill to the plugin\'s readers, writers and owners — and to nobody else', async () => {
103
+ const svc = await makeService({
104
+ ...BASE,
105
+ 'Skills/Eng/deploy/access.md': '---\n---\nread:\n - plugin/GTM/read\n',
106
+ });
107
+ const skill = 'Skills/Eng/deploy/SKILL.md';
108
+ // Group member, direct reader, writer (write implies read), owner.
109
+ for (const email of ['sam@x.io', 'ali@x.io', 'eng@x.io', 'owen@x.io']) {
110
+ expect(await svc.canRead(workspaceId, email, skill), email).toBe(true);
111
+ }
112
+ expect(await svc.canRead(workspaceId, 'mallory@x.io', skill)).toBe(false);
113
+ // Read on the plugin is read on the skill, nothing more.
114
+ expect(await svc.canWrite(workspaceId, 'sam@x.io', skill)).toBe(false);
115
+ });
116
+
117
+ it('the write token names the plugin\'s writers and owners only', async () => {
118
+ const svc = await makeService({
119
+ ...BASE,
120
+ 'Skills/Eng/deploy/access.md': '---\n---\nread:\n - plugin/GTM/read\nwrite:\n - plugin/GTM/write\n',
121
+ });
122
+ const skill = 'Skills/Eng/deploy/SKILL.md';
123
+ expect(await svc.canWrite(workspaceId, 'eng@x.io', skill)).toBe(true);
124
+ expect(await svc.canWrite(workspaceId, 'owen@x.io', skill)).toBe(true);
125
+ expect(await svc.canWrite(workspaceId, 'sam@x.io', skill)).toBe(false);
126
+ expect(await svc.canWrite(workspaceId, 'ali@x.io', skill)).toBe(false);
127
+ });
128
+
129
+ it('the owner token names owners alone', async () => {
130
+ const svc = await makeService({
131
+ ...BASE,
132
+ 'KnowledgeBase/Ops/access.md': '---\n---\nread:\n - plugin/GTM/owner\n',
133
+ });
134
+ expect(await svc.canRead(workspaceId, 'owen@x.io', 'KnowledgeBase/Ops/x.md')).toBe(true);
135
+ expect(await svc.canRead(workspaceId, 'eng@x.io', 'KnowledgeBase/Ops/x.md')).toBe(false);
136
+ });
137
+
138
+ it('follows the plugin\'s roster live: a member removed from the plugin loses the skill', async () => {
139
+ const files = {
140
+ ...BASE,
141
+ 'Skills/Eng/deploy/access.md': '---\n---\nread:\n - plugin/GTM/read\n',
142
+ };
143
+ const svc = await makeService(files);
144
+ const skill = 'Skills/Eng/deploy/SKILL.md';
145
+ expect(await svc.canRead(workspaceId, 'ali@x.io', skill)).toBe(true);
146
+
147
+ // Drop Ali from the plugin's own access.md — nothing on the skill changes.
148
+ await fs.writeFile(
149
+ path.join(root, workspaceId, KB_DIR, 'Plugins/GTM/access.md'),
150
+ pluginAccessMd('read:\n - Sales Team\nwrite:\n - Engineer\n'),
151
+ );
152
+ svc.invalidate(workspaceId);
153
+ expect(await svc.canRead(workspaceId, 'ali@x.io', skill)).toBe(false);
154
+ expect(await svc.canRead(workspaceId, 'sam@x.io', skill)).toBe(true);
155
+ });
156
+
157
+ it('a public WRITE principal makes the path writable by everyone — and the eligible set says so', async () => {
158
+ // A gate that counts approvers must not see "a principal with no
159
+ // enumerable members" where the truth is "anyone signed in".
160
+ const svc = await makeService({
161
+ ...BASE,
162
+ 'Plugins/Open/plugin.json': '{"name":"open"}',
163
+ 'Plugins/Open/access.md': pluginAccessMd('read:\n - everyone\nwrite:\n - everyone\n'),
164
+ 'Skills/Common/tips/access.md': '---\n---\nwrite:\n - plugin/Open/write\n',
165
+ });
166
+ const writers = await svc.eligibleWriters(workspaceId, 'Skills/Common/tips/SKILL.md');
167
+ expect(writers.roles).toContain('everyone');
168
+ expect(await svc.canWrite(workspaceId, 'nobody@elsewhere.io', 'Skills/Common/tips/SKILL.md')).toBe(true);
169
+ });
170
+
171
+ it('two folders folding to one slug: the first by path claims it, roster or none — a twin never supplies the members', async () => {
172
+ // Discovery keeps the first folder by path and skips its twin; the
173
+ // principal follows the same rule, so the twin's roster never becomes
174
+ // the grant's membership through the back door.
175
+ const svc = await makeService({
176
+ ...BASE,
177
+ 'Plugins/a/GTM/plugin.json': '{"name":"gtm"}',
178
+ 'Plugins/b/GTM/plugin.json': '{"name":"gtm"}',
179
+ 'Plugins/b/GTM/access.md': pluginAccessMd('read:\n - Zed <zed@x.io>\n'),
180
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - plugin/GTM/read\n',
181
+ });
182
+ // Path order is the walk's order: a/GTM comes first, claims the slug
183
+ // and has no roster — so the principal is EMPTY. Neither the root-level
184
+ // GTM's Ali nor the later twin's Zed gets in through it.
185
+ expect(await svc.canRead(workspaceId, 'ali@x.io', 'Skills/Common/tips/SKILL.md')).toBe(false);
186
+ expect(await svc.canRead(workspaceId, 'zed@x.io', 'Skills/Common/tips/SKILL.md')).toBe(false);
187
+ });
188
+
189
+ it('a public READ principal makes the path unrestricted — one truth for the flag and the lists', async () => {
190
+ const svc = await makeService({
191
+ ...BASE,
192
+ 'Plugins/Open/plugin.json': '{"name":"open"}',
193
+ 'Plugins/Open/access.md': pluginAccessMd('read:\n - everyone\nwrite:\n - Admin\n'),
194
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - plugin/Open/read\n',
195
+ });
196
+ const readers = await svc.eligibleReaders(workspaceId, 'Skills/Common/tips/SKILL.md');
197
+ expect(readers.restricted).toBe(false);
198
+ });
199
+
200
+ it('orders twins the way the walk does: component by component, never by the whole string', async () => {
201
+ // `a/b/GTM` is visited before `a-b/GTM` (the walk sorts each level:
202
+ // "a" < "a-b"); a whole-string compare would weigh "-" against "/" and
203
+ // could pick the other one — and with it the other roster.
204
+ const svc = await makeService({
205
+ 'roles.yaml': ROLES_YAML,
206
+ 'groups.yaml': GROUPS_YAML,
207
+ 'access.md': '---\nwrite:\n - Admin\n---\n',
208
+ 'Plugins/a/b/GTM/plugin.json': '{"name":"gtm"}',
209
+ 'Plugins/a/b/GTM/access.md': pluginAccessMd('read:\n - Ali <ali@x.io>\n'),
210
+ 'Plugins/a-b/GTM/plugin.json': '{"name":"gtm"}',
211
+ 'Plugins/a-b/GTM/access.md': pluginAccessMd('read:\n - Zed <zed@x.io>\n'),
212
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - plugin/GTM/read\n',
213
+ });
214
+ expect(await svc.canRead(workspaceId, 'ali@x.io', 'Skills/Common/tips/SKILL.md')).toBe(true);
215
+ expect(await svc.canRead(workspaceId, 'zed@x.io', 'Skills/Common/tips/SKILL.md')).toBe(false);
216
+ });
217
+
218
+ it('a public principal is a verdict, not a line: nothing spells everyone, so nothing under that name is removable', async () => {
219
+ const svc = await makeService({
220
+ ...BASE,
221
+ 'Plugins/Open/plugin.json': '{"name":"open"}',
222
+ 'Plugins/Open/access.md': pluginAccessMd('read:\n - everyone\nwrite:\n - Admin\n'),
223
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - plugin/Open/read\n',
224
+ });
225
+ const skill = 'Skills/Common/tips/SKILL.md';
226
+ const readers = await svc.eligibleReaders(workspaceId, skill);
227
+ expect(readers.restricted).toBe(false);
228
+ // Public — and the lists still name the grant that makes it so, so the
229
+ // share dialog has a row to remove it by.
230
+ expect(readers.principals).toEqual([
231
+ { name: 'Admin', kind: 'role' }, // the root's write, folded into read
232
+ { name: 'everyone', kind: 'role' },
233
+ { name: 'plugin/open/read', kind: 'plugin' },
234
+ ]);
235
+ // And WHY it is public: the resolver names the public plugin principal,
236
+ // which the lists alone cannot tell from an ordinary one.
237
+ expect(readers.publicVia).toEqual(['plugin/open/read']);
238
+ // A revoke of `everyone` here would strip no line — so no source claims one.
239
+ expect(await svc.grantSources(workspaceId, 'file', skill, { kind: 'role', role: 'everyone' })).toEqual({});
240
+ // The plugin grant IS the line, and the one whose removal ends public read.
241
+ expect(await svc.grantSources(workspaceId, 'file', skill, { kind: 'role', role: 'plugin/open/read' })).toEqual({
242
+ read: [{ kind: 'ancestor', path: 'Skills/Common/tips/access.md' }],
243
+ });
244
+ // A literal grant, by contrast, is a removable line.
245
+ const literal = await makeService({
246
+ ...BASE,
247
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - everyone\n',
248
+ });
249
+ expect(await literal.grantSources(workspaceId, 'file', skill, { kind: 'role', role: 'everyone' })).toEqual({
250
+ read: [{ kind: 'ancestor', path: 'Skills/Common/tips/access.md' }],
251
+ });
252
+ });
253
+
254
+ it('a DENIAL of a public principal denies everyone — it beats a literal everyone grant beside it and a farther one', async () => {
255
+ // Every NAMED person still reads (Admin is granted by name, and a grant
256
+ // wins among a caller's own keys), so re-checking named people finds
257
+ // nothing wrong; only the derived everyone verdict can say that the
258
+ // unnamed caller — who holds the public key and nothing else — is denied.
259
+ const svc = await makeService({
260
+ // The admin is the only named person in the whole knowledge base.
261
+ 'roles.yaml': 'roles:\n Admin:\n - admin@x.io\n',
262
+ 'groups.yaml': 'groups: {}\n',
263
+ 'access.md': '---\nwrite:\n - Admin\n---\n',
264
+ 'Plugins/Open/plugin.json': '{"name":"open"}',
265
+ 'Plugins/Open/access.md': pluginAccessMd('read:\n - everyone\nwrite:\n - Admin\n'),
266
+ // Public at the scope above; the folder itself carves everyone back out
267
+ // through the public principal, next to a literal grant that must lose.
268
+ 'Skills/Common/access.md': '---\n---\nread:\n - everyone\n',
269
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - everyone\n - Admin\n - deny plugin/Open/read\n',
270
+ });
271
+ const skill = 'Skills/Common/tips/SKILL.md';
272
+ expect(await svc.canRead(workspaceId, 'admin@x.io', skill)).toBe(true);
273
+ expect(await svc.canRead(workspaceId, 'nobody@elsewhere.io', skill)).toBe(false);
274
+ expect((await svc.eligibleReaders(workspaceId, skill)).restricted).toBe(true);
275
+ // The parent, where nothing denies, stays public.
276
+ expect((await svc.eligibleReaders(workspaceId, 'Skills/Common/SKILL.md')).restricted).toBe(false);
277
+ });
278
+
279
+ it('a plugin readable by everyone yields a public principal', async () => {
280
+ const svc = await makeService({
281
+ ...BASE,
282
+ 'Plugins/Open/plugin.json': '{"name":"open"}',
283
+ 'Plugins/Open/access.md': pluginAccessMd('read:\n - everyone\nwrite:\n - Admin\n'),
284
+ 'Skills/Common/tips/access.md': '---\n---\nread:\n - plugin/Open/read\n',
285
+ });
286
+ expect(await svc.canRead(workspaceId, 'nobody@elsewhere.io', 'Skills/Common/tips/SKILL.md')).toBe(true);
287
+ // The write token of that plugin is still its writers only.
288
+ expect(await svc.canWrite(workspaceId, 'nobody@elsewhere.io', 'Skills/Common/tips/SKILL.md')).toBe(false);
289
+ });
290
+
291
+ it('membership never counts the access.md frontmatter, a deny, or another plugin token', async () => {
292
+ const svc = await makeService({
293
+ ...BASE,
294
+ // Frontmatter `read: everyone` is discoverability of the FILE. The body
295
+ // denies Sue, and points at another plugin's token.
296
+ 'Plugins/Narrow/plugin.json': '{"name":"narrow"}',
297
+ 'Plugins/Narrow/access.md': pluginAccessMd(
298
+ 'read:\n - Sales Team\n - deny Sue <sue@x.io>\n - plugin/GTM/read\nwrite:\n - Admin\n',
299
+ ),
300
+ 'Skills/S/x/access.md': '---\n---\nread:\n - plugin/Narrow/read\n',
301
+ });
302
+ const skill = 'Skills/S/x/SKILL.md';
303
+ expect(await svc.canRead(workspaceId, 'sam@x.io', skill)).toBe(true);
304
+ // Deny entries are skipped, not subtracted: Sue is still in Sales Team.
305
+ expect(await svc.canRead(workspaceId, 'sue@x.io', skill)).toBe(true);
306
+ // GTM's reader Ali is NOT a member of Narrow through the nested token.
307
+ expect(await svc.canRead(workspaceId, 'ali@x.io', skill)).toBe(false);
308
+ // And nobody at random: the frontmatter `everyone` did not leak in.
309
+ expect(await svc.canRead(workspaceId, 'mallory@x.io', skill)).toBe(false);
310
+ });
311
+
312
+ it('a token naming a plugin that does not exist is ignored like an unknown role', async () => {
313
+ const svc = await makeService({
314
+ ...BASE,
315
+ 'Skills/S/x/access.md': '---\n---\nread:\n - plugin/Ghost/read\n - Ali <ali@x.io>\n',
316
+ });
317
+ expect(await svc.canRead(workspaceId, 'ali@x.io', 'Skills/S/x/SKILL.md')).toBe(true);
318
+ expect(await svc.canRead(workspaceId, 'sam@x.io', 'Skills/S/x/SKILL.md')).toBe(false);
319
+ });
320
+ });
321
+
322
+ describe('display', () => {
323
+ it('eligibleReaders reports the token as a plugin-kind principal spelled by the identity, whatever the grant wrote', async () => {
324
+ const svc = await makeService({
325
+ ...BASE,
326
+ // The folder's casing — how grants were written before the manifest became the identity.
327
+ 'Skills/Eng/deploy/access.md': '---\n---\nread:\n - plugin/GTM/read\n',
328
+ });
329
+ const readers = await svc.eligibleReaders(workspaceId, 'Skills/Eng/deploy');
330
+ expect(readers.restricted).toBe(true);
331
+ expect(readers.principals).toContainEqual({ name: 'plugin/gtm/read', kind: 'plugin' });
332
+ });
333
+
334
+ it('a plugin nested deeper than the root gets its principals too — a plugin is a folder with a manifest', async () => {
335
+ const svc = await makeService({
336
+ ...BASE,
337
+ 'Plugins/teams/Deep/plugin.json': '{"name":"deep"}',
338
+ 'Plugins/teams/Deep/access.md': pluginAccessMd('read:\n - Ali <ali@x.io>\n'),
339
+ // A scope folder's own access.md is NOT a roster: no manifest beside it.
340
+ 'Plugins/teams/access.md': '---\n---\nread:\n - everyone\n',
341
+ 'Skills/S/x/access.md': '---\n---\nread:\n - plugin/Deep/read\n - plugin/teams/read\n',
342
+ });
343
+ expect(await svc.canRead(workspaceId, 'ali@x.io', 'Skills/S/x/SKILL.md')).toBe(true);
344
+ expect(await svc.canRead(workspaceId, 'mallory@x.io', 'Skills/S/x/SKILL.md')).toBe(false);
345
+ });
346
+
347
+ it('kbPrincipals lists plugin folders once each, personal SHELVES excluded — by folder, never by name', async () => {
348
+ const svc = await makeService({
349
+ ...BASE,
350
+ 'Plugins/Ops/plugin.json': '{"name":"ops"}',
351
+ 'Plugins/Ops/access.md': pluginAccessMd('read:\n - Admin\n'),
352
+ 'Plugins/personal-abc123/plugin.json': '{"name":"personal-abc123"}',
353
+ 'Plugins/personal-abc123/access.md': '---\n---\nread:\n - Ali <ali@x.io>\n',
354
+ // A plugin whose hand-written manifest name wears the prefix is a
355
+ // plugin all the same: the shelf is a place, decided by the folder.
356
+ 'Plugins/Odd/plugin.json': '{"name":"personal-odd"}',
357
+ 'Plugins/Odd/access.md': pluginAccessMd('read:\n - Admin\n'),
358
+ });
359
+ const { plugins } = await svc.kbPrincipals(workspaceId);
360
+ // Named by identity — the manifest — with the folder beside it.
361
+ expect([...plugins].sort((a, b) => a.folder.localeCompare(b.folder))).toEqual([
362
+ { name: 'gtm', folder: 'Plugins/GTM' },
363
+ { name: 'personal-odd', folder: 'Plugins/Odd' },
364
+ { name: 'ops', folder: 'Plugins/Ops' },
365
+ ]);
366
+ });
367
+ });
368
+ });
@@ -184,6 +184,14 @@ describe('GroupsAdminService', () => {
184
184
  expect(await groupsYaml()).not.toContain('role/');
185
185
  });
186
186
 
187
+ it("createGroup refuses the reserved 'plugin/' prefix — a group could otherwise pose as a plugin's members", async () => {
188
+ await expect(service.createGroup(ADMIN, 'plugin/GTM/read')).rejects.toMatchObject({
189
+ status: 422,
190
+ message: expect.stringContaining('plugin/'),
191
+ });
192
+ expect(await groupsYaml()).not.toContain('plugin/');
193
+ });
194
+
187
195
  it('create → add → remove → delete lifecycle lands on disk', async () => {
188
196
  await service.createGroup(ADMIN, 'Contractors');
189
197
  await service.addMember(ADMIN, 'contractors', 'temp@x.io');
@@ -67,7 +67,7 @@ export type GrantPrincipal =
67
67
  * returns it, and payload consumers fall back to `roles` (all treated as
68
68
  * roles) when absent.
69
69
  */
70
- export type ResolvedPrincipal = { name: string; kind: 'role' | 'group' };
70
+ export type ResolvedPrincipal = { name: string; kind: 'role' | 'group' | 'plugin' };
71
71
 
72
72
  /**
73
73
  * Per-verb sources of a principal's access on a target. Only verbs the principal
@@ -199,11 +199,13 @@ export interface IAccessControl {
199
199
  /**
200
200
  * Answers the file viewer's "who can see this?" affordance. `restricted` is
201
201
  * false only when `read: everyone` applies without an effective user-level
202
- * denial — the node is readable by all signed-in users, and the role/user
203
- * lists are empty because their content would be meaningless. When
204
- * `restricted` is true the lists name the principals (roles + direct users)
205
- * that may read, with owners folded in (an `owner:` grant confers read). The
206
- * lists may be empty for a default-denied path with no grants.
202
+ * denial — the node is readable by all signed-in users. The lists name the
203
+ * principals (roles + direct users) granted read whether or not the node
204
+ * is public, with owners folded in (an `owner:` grant confers read); on a
205
+ * public node they include what makes it public. `publicVia` names the
206
+ * PUBLIC plugin principals granted read here — principals every signed-in
207
+ * user holds — so a caller can tell public-through-a-plugin from a literal
208
+ * `everyone` grant. The lists may be empty for a default-denied path.
207
209
  */
208
210
  eligibleReaders(
209
211
  workspaceId: string,
@@ -213,6 +215,7 @@ export interface IAccessControl {
213
215
  principals?: ResolvedPrincipal[];
214
216
  roles: string[];
215
217
  users: { name: string; email: string }[];
218
+ publicVia?: string[];
216
219
  }>;
217
220
 
218
221
  /**
@@ -323,9 +326,17 @@ export interface IAccessControl {
323
326
  * (named). The login-only `users` table is unioned in by the caller — this
324
327
  * method covers the KB-canonical people the users table misses.
325
328
  */
326
- kbPrincipals(
327
- workspaceId: string,
328
- ): Promise<{ roles: string[]; groups: string[]; people: { name: string; email: string }[] }>;
329
+ kbPrincipals(workspaceId: string): Promise<{
330
+ roles: string[];
331
+ groups: string[];
332
+ /**
333
+ * Plugins whose `plugin/<Name>/<verb>` principals exist (personal folders
334
+ * excluded): the display name and the repo-relative folder, so a caller
335
+ * can apply the discoverability verdict (`canRead` on `<folder>/access.md`).
336
+ */
337
+ plugins: { name: string; folder: string }[];
338
+ people: { name: string; email: string }[];
339
+ }>;
329
340
 
330
341
  /**
331
342
  * Reverse-lookup an email by its SHA-256 hash (per `hashEmail` semantics)