@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
@@ -3,7 +3,11 @@ import path from 'node:path';
3
3
  import { execFile, spawn } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
  import { createHash } from 'node:crypto';
6
+ import { isAbsence } from '../../shared/fs-errors.js';
7
+ import { walkKb } from '../../shared/kb-walk.js';
8
+ import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, isPersonalPluginDir, pluginIdentityOf, } from '@bevel-software/platform-shared';
6
9
  import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
10
+ import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
7
11
  import { GROUPS_YAML, SYNCED_GROUPS_YAML, parseGroupsFile, } from '../access-model/group-files.js';
8
12
  import { ADMIN_CANONICAL, EVERYONE_CANONICAL, EVERYONE_DISPLAY, KNOWN_VERBS, ROLE_TOKEN_PREFIX, accessMdSelfEntries, canonicalEmail, canonicalRoleName, isAccessMdPath, mergeGroupsIntoRoles, parseAccessFile, parseOwnAccessEntries, parseRolesYaml, sourceVerbsFor, } from '../access-model/access-grammar.js';
9
13
  import { DIRECTORY_SYNC_BOT_EMAIL, DIRECTORY_SYNC_BOT_NAME, } from './directory-sync-bot.js';
@@ -78,11 +82,6 @@ function isBuiltInRole(canonicalRole) {
78
82
  function roleKnown(roles, canonicalRole) {
79
83
  return roles.byCanonical.has(canonicalRole) || isBuiltInRole(canonicalRole);
80
84
  }
81
- /** True for the errno codes that mean "the file genuinely is not there". */
82
- function isAbsenceError(err) {
83
- const code = err?.code;
84
- return code === 'ENOENT' || code === 'ENOTDIR';
85
- }
86
85
  /**
87
86
  * Load the ACTIVE group source through `read` (working tree or at-ref — the
88
87
  * caller supplies the reader, so both model loaders share one mode rule):
@@ -119,7 +118,7 @@ export async function loadActiveGroups(read) {
119
118
  // the same error; groups cannot be the one input that does not.
120
119
  if (err instanceof AccessUnreadableError)
121
120
  throw err;
122
- if (isAbsenceError(err)) {
121
+ if (isAbsence(err)) {
123
122
  syncedText = null;
124
123
  }
125
124
  else {
@@ -141,7 +140,7 @@ export async function loadActiveGroups(read) {
141
140
  catch (err) {
142
141
  if (err instanceof AccessUnreadableError)
143
142
  throw err; // see above
144
- if (isAbsenceError(err))
143
+ if (isAbsence(err))
145
144
  text = null;
146
145
  else
147
146
  return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
@@ -165,6 +164,64 @@ export async function loadActiveGroups(read) {
165
164
  * `hasPermissionResolved`). `collapseScopes` flattens this into the
166
165
  * closest-wins-per-principal view the display helpers use.
167
166
  */
167
+ /**
168
+ * The rules listener on the one walk of the checkout: every `access.md` at
169
+ * any depth (the access tree is structure-agnostic — the root's included),
170
+ * plus every plugin manifest under the plugins root, for principal
171
+ * synthesis. A reader: a folder the walk could not list is simply not in
172
+ * the model (a writer that needs completeness asks the walk for its holes).
173
+ *
174
+ * Per-file failures (malformed YAML, bad role refs, unknown verbs) are
175
+ * logged and the offending file is dropped from the model — they do NOT
176
+ * throw. A typo in one nested access.md must not 500 the entire editor;
177
+ * admins can still write `access.md` / `roles.yaml` because
178
+ * `hasPermissionResolved` admin-rescues those paths, so the bad config
179
+ * remains fixable from inside the app.
180
+ */
181
+ function accessFileListener(repoDir, out, pluginDirs) {
182
+ return {
183
+ async onFile(dir, name) {
184
+ const rel = dir ? `${dir}/${name}` : name;
185
+ if (name === PLUGIN_MANIFEST_FILE && dir.startsWith(`${PLUGINS_DIR}/`)) {
186
+ const text = await fs.readFile(path.join(repoDir, rel), 'utf-8').catch(() => null);
187
+ pluginDirs.set(dir, pluginIdentityOf(parseManifestText(text), path.posix.basename(dir)));
188
+ return;
189
+ }
190
+ if (name !== 'access.md')
191
+ return;
192
+ const text = await fs.readFile(path.join(repoDir, rel), 'utf-8');
193
+ const parsed = parseAccessFile(text, rel);
194
+ if (!parsed.ok) {
195
+ // Treat as if the file didn't exist for resolution purposes.
196
+ // Admin-rescue on access.md paths still lets an admin fix it.
197
+ for (const e of parsed.errors)
198
+ console.warn(`[access] ${e} — file ignored`);
199
+ return;
200
+ }
201
+ for (const w of parsed.warnings)
202
+ console.warn(`[access] ${w}`);
203
+ if (out.has(parsed.file.dir)) {
204
+ console.warn(`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
205
+ return;
206
+ }
207
+ out.set(parsed.file.dir, parsed.file);
208
+ },
209
+ };
210
+ }
211
+ /** A manifest's parsed object, or null for absent, unparsable or not-an-object text. */
212
+ function parseManifestText(text) {
213
+ if (text === null)
214
+ return null;
215
+ try {
216
+ const parsed = JSON.parse(text);
217
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
218
+ ? parsed
219
+ : null;
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ }
168
225
  function resolveScopes(model, verb, relativePath, fileOwn) {
169
226
  const target = verb;
170
227
  const supersets = sourceVerbsFor(verb).filter((v) => v !== verb);
@@ -197,7 +254,32 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
197
254
  set(entry, 'grant');
198
255
  }
199
256
  }
200
- return { byRole, byEmail, source };
257
+ // The anonymous caller's verdict at this scope — what someone who holds
258
+ // exactly the PUBLIC keys (a plugin principal whose plugin admits
259
+ // everyone, so everyone holds it) resolves to, by the same tiers a named
260
+ // caller gets: a grant via any public key wins, else a denial via one
261
+ // denies, else the built-in `everyone` entry decides. Derived HERE, where
262
+ // a scope's verdicts are built, so every reader of it — the permission
263
+ // check, the eligible lists, the `restricted` flag, the compiler's
264
+ // everyone audience — sees one truth; and derived into its own field,
265
+ // never into `byRole`, so the entries a person can remove stay exactly
266
+ // the lines the files hold.
267
+ let publicGrant = false;
268
+ let publicDeny = false;
269
+ for (const [key, state] of byRole) {
270
+ if (!model.roles.publicKeys?.has(key))
271
+ continue;
272
+ if (state === 'grant')
273
+ publicGrant = true;
274
+ else if (state === 'denied')
275
+ publicDeny = true;
276
+ }
277
+ const everyone = publicGrant
278
+ ? 'grant'
279
+ : publicDeny
280
+ ? 'denied'
281
+ : byRole.get(EVERYONE_CANONICAL);
282
+ return { byRole, byEmail, everyone, source };
201
283
  };
202
284
  const scopes = [];
203
285
  // The node's own frontmatter is the most specific scope. Role refs there are
@@ -221,17 +303,33 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
221
303
  function collapseScopes(scopes) {
222
304
  const byRole = new Map();
223
305
  const byEmail = new Map();
306
+ let everyone;
224
307
  for (let i = scopes.length - 1; i >= 0; i--) {
225
308
  for (const [k, v] of scopes[i].byRole)
226
309
  byRole.set(k, v);
227
310
  for (const [k, v] of scopes[i].byEmail)
228
311
  byEmail.set(k, v);
312
+ if (scopes[i].everyone !== undefined)
313
+ everyone = scopes[i].everyone;
229
314
  }
230
- return { byRole, byEmail };
315
+ return { byRole, byEmail, everyone };
231
316
  }
232
317
  function resolveAtPath(model, verb, relativePath, fileOwn) {
233
318
  return collapseScopes(resolveScopes(model, verb, relativePath, fileOwn));
234
319
  }
320
+ /**
321
+ * Every principal key a caller holds: the tokens their email is a member of,
322
+ * plus the PUBLIC keys every caller holds regardless (a plugin principal
323
+ * whose plugin grants `everyone`). Undefined when there is nothing at all,
324
+ * matching the plain `byEmail.get` the tier-2 check used to read.
325
+ */
326
+ function principalKeysOf(model, email) {
327
+ const own = model.roles.byEmail.get(email);
328
+ const pub = model.roles.publicKeys;
329
+ if (!pub?.size)
330
+ return own;
331
+ return new Set([...(own ?? []), ...pub]);
332
+ }
235
333
  function isAdminEmail(model, email) {
236
334
  if (model.deploymentOwners.has(email))
237
335
  return true;
@@ -282,7 +380,7 @@ function hasPermissionResolved(model, verb, userEmail, relativePath, fileOwn) {
282
380
  if (isAccessMdPath(relativePath) && isAdminEmail(model, email))
283
381
  return true;
284
382
  }
285
- const userRoles = model.roles.byEmail.get(email);
383
+ const userRoles = principalKeysOf(model, email);
286
384
  const scopes = resolveScopes(model, verb, relativePath, fileOwn);
287
385
  for (const scope of scopes) {
288
386
  // Tier 1 — direct email entry is the most specific verdict at this scope.
@@ -307,10 +405,10 @@ function hasPermissionResolved(model, verb, userEmail, relativePath, fileOwn) {
307
405
  if (deny)
308
406
  return false;
309
407
  }
310
- // Tier 3 — the built-in `everyone` role.
311
- const everyone = scope.byRole.get(EVERYONE_CANONICAL);
312
- if (everyone)
313
- return everyone === 'grant';
408
+ // Tier 3 — the built-in `everyone` role (derived: a public plugin
409
+ // principal granted here counts, see `AccessScope.everyone`).
410
+ if (scope.everyone)
411
+ return scope.everyone === 'grant';
314
412
  // No verdict at this scope — fall through to the next (farther) one.
315
413
  }
316
414
  return false;
@@ -454,11 +552,11 @@ function dirChainFor(relativePath) {
454
552
  * reported as restricted.
455
553
  */
456
554
  function canEveryoneReadResolved(model, relativePath, fileOwn) {
457
- const { byRole, byEmail } = resolveAtPath(model, 'read', relativePath, fileOwn);
555
+ const { byEmail, everyone } = resolveAtPath(model, 'read', relativePath, fileOwn);
458
556
  // Baseline: an unnamed signed-in user (no email/role entries) reads only via
459
557
  // `everyone`. As a single principal, its collapsed verdict is its closest —
460
558
  // exactly what that user resolves to.
461
- if (byRole.get(EVERYONE_CANONICAL) !== 'grant')
559
+ if (everyone !== 'grant')
462
560
  return false;
463
561
  // Every *named* principal must also still read. A collapsed deny may have
464
562
  // been shadowed by a closer-scope `everyone` grant, so re-resolve each
@@ -491,7 +589,7 @@ function canReadResolved(model, userEmail, relativePath, fileOwn) {
491
589
  return hasPermissionResolved(model, 'read', userEmail, relativePath, fileOwn);
492
590
  }
493
591
  function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
494
- const { byRole, byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
592
+ const { byRole, byEmail, everyone } = resolveAtPath(model, verb, relativePath, fileOwn);
495
593
  // (kind, display name) → one entry. The `byRole` keys are the merged
496
594
  // index's canonical tokens exactly as granted (bare, or the
497
595
  // `role/<canonical>` alias); the `byCanonical` record a key hits carries
@@ -515,6 +613,12 @@ function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
515
613
  const record = model.roles.byCanonical.get(canonical);
516
614
  addPrincipal(record ? record.displayName : canonical, record?.kind ?? 'role');
517
615
  }
616
+ // The derived everyone verdict: a public plugin principal granted here
617
+ // means anyone signed in holds the verb, and a list that counts holders (an
618
+ // approval gate, a "restricted to" banner) must say so — as `everyone`, the
619
+ // same row a literal grant produces.
620
+ if (everyone === 'grant')
621
+ addPrincipal(EVERYONE_CANONICAL, 'role');
518
622
  // Mirror the admin overrides applied in `hasPermissionResolved`: write on
519
623
  // `roles.yaml` and on any `access.md` is granted to Admin even if the
520
624
  // file content doesn't list it. Without surfacing that here, the
@@ -674,7 +778,29 @@ export class AccessControlService {
674
778
  invalidate(workspaceId) {
675
779
  this.cache.delete(workspaceId);
676
780
  this.ownEntriesCache.delete(workspaceId);
781
+ this.generation++;
677
782
  }
783
+ /**
784
+ * Bumped by every `invalidate()`, for any workspace. A model load reads the
785
+ * tree across many awaits, and an invalidation fires exactly when that tree
786
+ * changes underneath it: the load that started on the old tree would
787
+ * otherwise store its model AFTER the drop, and every read for the next TTL
788
+ * would resolve against a tree that is already gone. `loadModel` takes the
789
+ * generation before its first read and refuses to store a model fetched
790
+ * under an older one — the caller that started it still gets its answer,
791
+ * as old as the moment it started, and the next caller reloads.
792
+ *
793
+ * ONE counter, not one per workspace: a per-workspace map would keep an
794
+ * entry for every branch workspace the process ever loaded, with nothing to
795
+ * tell it when a workspace is gone. The price of sharing is that a drop on
796
+ * some other workspace, landing mid-load, costs this one a single reload —
797
+ * a load is tens of milliseconds and a drop is a commit, so that is rare
798
+ * and cheap, and it never stores a wrong model. (The own-entries memo needs
799
+ * no token: it is keyed by the map object a reader took BEFORE its disk
800
+ * read, and `invalidate()` replaces the map, so a late store lands in an
801
+ * orphan nobody consults.)
802
+ */
803
+ generation = 0;
678
804
  /** Memoized `readOwnEntries` for the batch paths; see `ownEntriesCache`. */
679
805
  async cachedOwnEntries(workspaceId, repoDir, relativePath) {
680
806
  let ws = this.ownEntriesCache.get(workspaceId);
@@ -773,14 +899,26 @@ export class AccessControlService {
773
899
  async eligibleReaders(workspaceId, relativePath) {
774
900
  const model = await this.loadModel(workspaceId);
775
901
  const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
776
- // When `read: everyone` applies cleanly, the node is readable by all users
777
- // and the role/user lists are meaningless. Otherwise return the explicit
778
- // reader set; it may be empty for a default-denied path with no grants.
779
- if (canEveryoneReadResolved(model, relativePath, own)) {
780
- return { restricted: false, principals: [], roles: [], users: [] };
781
- }
902
+ // `restricted` is the verdict — whether `read: everyone` applies cleanly —
903
+ // and the lists are the GRANTS that exist regardless: on a public node
904
+ // they name what makes it public (`everyone`, and a public plugin
905
+ // principal whose grant is the one to remove), so the share dialog can
906
+ // show that grant as a row. Consumers that only want to know whether the
907
+ // node is public read the flag; the lists may be empty for a
908
+ // default-denied path with no grants.
782
909
  const { principals, roles, users } = eligibleHoldersResolved(model, 'read', relativePath, own);
783
- return { restricted: true, principals, roles, users };
910
+ // WHY it is public, when it is: the public plugin principals granted read
911
+ // here — the one thing the dialog cannot tell from the lists alone (a
912
+ // plugin principal in them may or may not be public), so the resolver,
913
+ // which knows, says it. A literal `everyone` line shows up as a source
914
+ // of the `everyone` row instead; together the two answer "what remains
915
+ // public after this grant is removed".
916
+ const collapsed = resolveAtPath(model, 'read', relativePath, own);
917
+ const publicVia = [...collapsed.byRole]
918
+ .filter(([key, state]) => state === 'grant' && model.roles.publicKeys?.has(key))
919
+ .map(([key]) => key)
920
+ .sort();
921
+ return { restricted: !canEveryoneReadResolved(model, relativePath, own), principals, roles, users, publicVia };
784
922
  }
785
923
  async eligibleDownloaders(workspaceId, relativePath) {
786
924
  const model = await this.loadModel(workspaceId);
@@ -819,7 +957,7 @@ export class AccessControlService {
819
957
  model = await this.loadModel(workspaceId);
820
958
  }
821
959
  catch {
822
- return { roles: [], groups: [], people: [] };
960
+ return { roles: [], groups: [], plugins: [], people: [] };
823
961
  }
824
962
  // Roles = the built-in `everyone` role plus every declared role's display
825
963
  // name — ROLE principals only, never groups. Each role is enumerated via
@@ -831,11 +969,22 @@ export class AccessControlService {
831
969
  // direct-access.md edit).
832
970
  const roles = [EVERYONE_DISPLAY];
833
971
  const groups = [];
972
+ // Plugins = the identities behind the synthesised `plugin/<name>/<verb>`
973
+ // principals, once each (three keys share a folder). Personal shelves are
974
+ // plugins to the resolver but not to a person picking a grantee — told
975
+ // apart by their FOLDER (the one structural rule), never by their name.
976
+ const plugins = new Map();
834
977
  for (const [key, principal] of model.roles.byCanonical) {
835
978
  if (key.startsWith(ROLE_TOKEN_PREFIX))
836
979
  roles.push(principal.displayName);
837
980
  else if (principal.kind === 'group')
838
981
  groups.push(principal.displayName);
982
+ else if (principal.kind === 'plugin' &&
983
+ principal.pluginName &&
984
+ principal.pluginDir &&
985
+ !isPersonalPluginDir(principal.pluginDir)) {
986
+ plugins.set(principal.pluginName, principal.pluginDir);
987
+ }
839
988
  }
840
989
  // People = roles.yaml member emails (name-less) ∪ access.md `Name <email>`
841
990
  // grants (named). The login-only users table is unioned in by the caller.
@@ -857,7 +1006,14 @@ export class AccessControlService {
857
1006
  name: name || email.split('@')[0],
858
1007
  email,
859
1008
  }));
860
- return { roles, groups, people };
1009
+ return {
1010
+ roles,
1011
+ groups,
1012
+ plugins: [...plugins.entries()]
1013
+ .map(([name, folder]) => ({ name, folder }))
1014
+ .sort((a, b) => a.name.localeCompare(b.name)),
1015
+ people,
1016
+ };
861
1017
  }
862
1018
  async findEmailByHash(workspaceId, hash) {
863
1019
  let model;
@@ -1012,6 +1168,8 @@ export class AccessControlService {
1012
1168
  if (cached && Date.now() - cached.loadedAt < AccessControlService.CACHE_TTL_MS) {
1013
1169
  return cached.model;
1014
1170
  }
1171
+ // Taken before the first read; see `generation`.
1172
+ const generation = this.generation;
1015
1173
  const repoDir = await this.repoDir(workspaceId);
1016
1174
  let rolesYaml;
1017
1175
  try {
@@ -1063,7 +1221,14 @@ export class AccessControlService {
1063
1221
  // must not 500 the entire editor; admins can still write `access.md`
1064
1222
  // / `roles.yaml` because `hasPermissionResolved` admin-rescues those
1065
1223
  // paths, so the bad config remains fixable from inside the app.
1066
- await this.collectAccessFiles(repoDir, '', accessFiles);
1224
+ const pluginDirs = new Map();
1225
+ await walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
1226
+ // Plugin principals (`plugin/<Name>/<verb>`), derived from each plugin
1227
+ // folder's own access.md — a plugin being a folder with a manifest, at
1228
+ // any depth — after groups merged (so their rosters expand through the
1229
+ // finished index) and before the unknown-role sweep below (so a grant
1230
+ // naming one counts as known).
1231
+ synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginDirs);
1067
1232
  // Validate role refs against roles.yaml. `everyone` is a built-in role and
1068
1233
  // is valid without a roles.yaml entry. Unknown refs are dropped from the
1069
1234
  // parsed entry list (along with a warn log) so the rest of the file still
@@ -1085,49 +1250,12 @@ export class AccessControlService {
1085
1250
  groupsHealth: activeGroups.health,
1086
1251
  deploymentOwners: this.deploymentOwners,
1087
1252
  };
1088
- this.cache.set(workspaceId, { model, loadedAt: Date.now() });
1089
- return model;
1090
- }
1091
- async collectAccessFiles(absDir, relDir, out) {
1092
- let entries;
1093
- try {
1094
- entries = await fs.readdir(absDir, { withFileTypes: true });
1095
- }
1096
- catch {
1097
- return;
1098
- }
1099
- for (const entry of entries) {
1100
- // Skip VCS metadata + vendored deps. Hidden dirs (`.git`, `.vscode`,
1101
- // etc.) and `node_modules` can't host KB rules and are often huge —
1102
- // walking them would slow every cache miss without benefit.
1103
- if (entry.name.startsWith('.'))
1104
- continue;
1105
- if (entry.name === 'node_modules')
1106
- continue;
1107
- const abs = path.join(absDir, entry.name);
1108
- const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
1109
- if (entry.isDirectory()) {
1110
- await this.collectAccessFiles(abs, rel, out);
1111
- }
1112
- else if (entry.isFile() && entry.name === 'access.md') {
1113
- const text = await fs.readFile(abs, 'utf-8');
1114
- const parsed = parseAccessFile(text, rel);
1115
- if (!parsed.ok) {
1116
- // Treat as if the file didn't exist for resolution purposes.
1117
- // Admin-rescue on access.md paths still lets an admin fix it.
1118
- for (const e of parsed.errors)
1119
- console.warn(`[access] ${e} — file ignored`);
1120
- continue;
1121
- }
1122
- for (const w of parsed.warnings)
1123
- console.warn(`[access] ${w}`);
1124
- if (out.has(parsed.file.dir)) {
1125
- console.warn(`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
1126
- continue;
1127
- }
1128
- out.set(parsed.file.dir, parsed.file);
1129
- }
1253
+ // An invalidation landed mid-load: this model describes a tree that is
1254
+ // already gone, so it is returned to this caller but never stored.
1255
+ if (this.generation === generation) {
1256
+ this.cache.set(workspaceId, { model, loadedAt: Date.now() });
1130
1257
  }
1258
+ return model;
1131
1259
  }
1132
1260
  async repoDir(workspaceId) {
1133
1261
  const wsDir = await this.workspaceService.getWorkspacePath(workspaceId);
@@ -1255,14 +1383,18 @@ export class AccessControlService {
1255
1383
  try {
1256
1384
  const { stdout } = await execFileAsync('git', ['-C', repoDir, 'ls-tree', '-r', '--name-only', ref], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
1257
1385
  const out = [];
1386
+ const pluginDirs = [];
1258
1387
  for (const line of stdout.split('\n')) {
1259
1388
  const p = line.trim();
1260
1389
  if (!p)
1261
1390
  continue;
1262
1391
  if (p === 'access.md' || p.endsWith('/access.md'))
1263
1392
  out.push(p);
1393
+ else if (p.startsWith(`${PLUGINS_DIR}/`) && p.endsWith(`/${PLUGIN_MANIFEST_FILE}`)) {
1394
+ pluginDirs.push(p.slice(0, -(PLUGIN_MANIFEST_FILE.length + 1)));
1395
+ }
1264
1396
  }
1265
- return out;
1397
+ return { accessFiles: out, pluginDirs };
1266
1398
  }
1267
1399
  catch {
1268
1400
  return 'error';
@@ -1407,7 +1539,7 @@ export class AccessControlService {
1407
1539
  console.warn(`[access@${tag}] listing access.md files failed; refusing to decide from a partial tree`);
1408
1540
  throw new AccessUnreadableError(label, 'access.md (ls-tree)');
1409
1541
  }
1410
- for (const p of listed) {
1542
+ for (const p of listed.accessFiles) {
1411
1543
  const text = await read(p);
1412
1544
  if (text === null)
1413
1545
  continue;
@@ -1419,6 +1551,16 @@ export class AccessControlService {
1419
1551
  // duplicates, but be defensive.
1420
1552
  accessFiles.set(parsed.file.dir, parsed.file);
1421
1553
  }
1554
+ // Mirror `loadModel`: plugin principals from the plugins the ref carries,
1555
+ // each under the identity its manifest AT THAT REF declares, so a PR-time
1556
+ // verdict on a skill granted to `plugin/<name>/write` counts the plugin's
1557
+ // writers exactly as the working tree would.
1558
+ const pluginIdentities = new Map();
1559
+ for (const dir of listed.pluginDirs) {
1560
+ const manifest = parseManifestText(await read(`${dir}/${PLUGIN_MANIFEST_FILE}`));
1561
+ pluginIdentities.set(dir, pluginIdentityOf(manifest, path.posix.basename(dir)));
1562
+ }
1563
+ synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginIdentities);
1422
1564
  // Mirror `loadModel`: drop entries whose role ref is unknown to roles.yaml,
1423
1565
  // while preserving the built-in `everyone` role.
1424
1566
  for (const [, file] of accessFiles) {