@bevel-software/platform-core-backend 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -1,10 +1,11 @@
1
1
  import express from 'express';
2
2
  import type { AuthUser } from '@bevel-software/platform-shared';
3
- import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
3
+ import { isProtectedBranch, DEFAULT_BRANCH, pluginManifestName } from '@bevel-software/platform-shared';
4
4
  import type {
5
5
  IAccessControl,
6
6
  GrantPrincipal,
7
7
  GrantSources,
8
+ ResolvedPrincipal,
8
9
  } from './access-control.interface.js';
9
10
  import type { WorkspaceService } from '../workspace/workspace.service.js';
10
11
  import { branchForWorkspaceId, workspaceIdForBranch } from '../../shared/workspace-id.js';
@@ -22,7 +23,10 @@ import {
22
23
  import {
23
24
  canonicalRoleName,
24
25
  EVERYONE_CANONICAL,
26
+ PLUGIN_TOKEN_PREFIX,
27
+ PLUGIN_TOKEN_VERBS,
25
28
  ROLE_TOKEN_PREFIX,
29
+ type PluginTokenVerb,
26
30
  type Verb,
27
31
  } from '../access-model/access-grammar.js';
28
32
  import { listAccessDeclarationsUnder } from './access-declarations.js';
@@ -79,20 +83,51 @@ export function createAccessRoutes(
79
83
  // accepts — the same principals the admin screens manage, regardless of
80
84
  // which branch the caller is viewing. People stay branch-local (the
81
85
  // caller's own workspace).
82
- const defaultBranchPrincipals = async (): Promise<{ roles: string[]; groups: string[] }> => {
86
+ const defaultBranchPrincipals = async (): Promise<{
87
+ roles: string[];
88
+ groups: string[];
89
+ plugins: { name: string; folder: string }[];
90
+ }> => {
83
91
  await workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
84
- const { roles, groups } = await accessControl.kbPrincipals(workspaceIdForBranch(DEFAULT_BRANCH));
85
- return { roles, groups };
92
+ const { roles, groups, plugins } = await accessControl.kbPrincipals(
93
+ workspaceIdForBranch(DEFAULT_BRANCH),
94
+ );
95
+ // `plugins` is defensive: an older resolver double may omit it.
96
+ return { roles, groups, plugins: plugins ?? [] };
86
97
  };
87
98
 
88
99
  /**
89
- * What the mutation routes accept as a principal. `group` exists only at
90
- * this boundary: in the access.md entry grammar a group grant is a
91
- * bare-name token (bare names resolve GROUP-FIRST, then fall back to the
92
- * role), so it is spliced as a role-shaped principal — the separate kind
93
- * buys validation against the right namespace and honest 404s.
100
+ * The plugins `email` may DISCOVER — read the folder's access.md, the same
101
+ * verdict the plugin index lists on. Both the suggest list and the grant
102
+ * route go through this, so a plugin hidden from the picker cannot be
103
+ * named in a grant either, and an undiscoverable one answers exactly like
104
+ * one that does not exist.
94
105
  */
95
- type RoutePrincipal = Principal | { kind: 'group'; group: string };
106
+ const discoverablePlugins = async (
107
+ email: string,
108
+ plugins: { name: string; folder: string }[],
109
+ ): Promise<{ name: string; folder: string }[]> => {
110
+ if (plugins.length === 0) return [];
111
+ const verdicts = await accessControl.canReadBatch(
112
+ workspaceIdForBranch(DEFAULT_BRANCH),
113
+ email,
114
+ plugins.map((p) => `${p.folder}/access.md`),
115
+ );
116
+ return plugins.filter((p) => verdicts.get(`${p.folder}/access.md`) === true);
117
+ };
118
+
119
+ /**
120
+ * What the mutation routes accept as a principal. `group` and `plugin`
121
+ * exist only at this boundary: in the access.md entry grammar a group grant
122
+ * is a bare-name token (bare names resolve GROUP-FIRST, then fall back to
123
+ * the role) and a plugin grant is the `plugin/<Name>/<verb>` token, so both
124
+ * are spliced as role-shaped principals — the separate kinds buy validation
125
+ * against the right namespace and honest 404s.
126
+ */
127
+ type RoutePrincipal =
128
+ | Principal
129
+ | { kind: 'group'; group: string }
130
+ | { kind: 'plugin'; plugin: string; verb: PluginTokenVerb };
96
131
 
97
132
  /**
98
133
  * The role-shaped principal the splice layer actually writes/matches.
@@ -104,6 +139,9 @@ export function createAccessRoutes(
104
139
  */
105
140
  const asSplicePrincipal = (p: RoutePrincipal): Principal => {
106
141
  if (p.kind === 'group') return { kind: 'role', role: p.group };
142
+ // Written with the folder's own casing; `parseAccessEntry` canonicalises
143
+ // the name half to the manifest slug on the way back in.
144
+ if (p.kind === 'plugin') return { kind: 'role', role: `${PLUGIN_TOKEN_PREFIX}${p.plugin.trim()}/${p.verb}` };
107
145
  if (p.kind === 'role') {
108
146
  const canonical = canonicalRoleName(p.role);
109
147
  const bare = canonical.startsWith(ROLE_TOKEN_PREFIX)
@@ -336,7 +374,7 @@ export function createAccessRoutes(
336
374
  // Independent lookups batched in ONE Promise.all: the default-branch
337
375
  // principals (cached model), the branch-local KB people (cached model),
338
376
  // and the users table (only consulted once the query is long enough).
339
- const [{ roles, groups }, { people: kbPeople }, userRows] = await Promise.all([
377
+ const [{ roles, groups, plugins }, { people: kbPeople }, userRows] = await Promise.all([
340
378
  defaultBranchPrincipals(),
341
379
  accessControl.kbPrincipals(req.params.id),
342
380
  q.length >= 2 ? db.select().from(users) : Promise.resolve([]),
@@ -350,6 +388,19 @@ export function createAccessRoutes(
350
388
  .filter((g) => !q || g.toLowerCase().includes(q))
351
389
  .slice(0, CAP);
352
390
 
391
+ // Plugins the caller can DISCOVER (read the folder's access.md — the
392
+ // same verdict the plugin index lists on), so the picker never names a
393
+ // plugin its enumeration would hide. Their `plugin/<Name>/<verb>`
394
+ // tokens are built client-side; the name is what a person searches for.
395
+ const matchedPlugins = (
396
+ await discoverablePlugins(
397
+ user.email,
398
+ plugins.filter((p) => !q || p.name.toLowerCase().includes(q)),
399
+ )
400
+ )
401
+ .map((p) => p.name)
402
+ .slice(0, CAP);
403
+
353
404
  let people: { name: string; email: string }[] = [];
354
405
  if (q.length >= 2) {
355
406
  // Union the KB-canonical people with the login-only users table.
@@ -373,6 +424,10 @@ export function createAccessRoutes(
373
424
  res.json({
374
425
  roles: matchedRoles,
375
426
  groups: matchedGroups,
427
+ // Plugin FOLDER names; each stands for three grantable principals
428
+ // (`plugin/<Name>/read|write|owner`). Not `plugins` — that key is the
429
+ // deprecated alias of `roles` below.
430
+ pluginPrincipals: matchedPlugins,
376
431
  people,
377
432
  peopleWithheld: q.length < 2,
378
433
  // DEPRECATED alias of `roles` — the shipped share dialog still reads
@@ -428,8 +483,19 @@ export function createAccessRoutes(
428
483
  throw new AccessMutationError('group principal needs a group name');
429
484
  }
430
485
  principal = { kind: 'group', group: principalRaw.group };
486
+ } else if (principalRaw.kind === 'plugin') {
487
+ if (typeof principalRaw.plugin !== 'string' || !principalRaw.plugin.trim()) {
488
+ throw new AccessMutationError('plugin principal needs a plugin name');
489
+ }
490
+ const verb = principalRaw.verb;
491
+ if (typeof verb !== 'string' || !(PLUGIN_TOKEN_VERBS as readonly string[]).includes(verb)) {
492
+ throw new AccessMutationError(
493
+ `plugin principal needs a verb: one of ${PLUGIN_TOKEN_VERBS.join(', ')}`,
494
+ );
495
+ }
496
+ principal = { kind: 'plugin', plugin: principalRaw.plugin, verb: verb as PluginTokenVerb };
431
497
  } else {
432
- throw new AccessMutationError("principal.kind must be 'user', 'group', or 'role'");
498
+ throw new AccessMutationError("principal.kind must be 'user', 'group', 'plugin', or 'role'");
433
499
  }
434
500
  const targetKind = kind as TargetKind;
435
501
  const repoRelTarget = toRepoRelative(rawPath);
@@ -517,12 +583,12 @@ export function createAccessRoutes(
517
583
  // non-actionable. Built over the union of every principal in the four
518
584
  // eligible lists (kinded `principals`, with the name-only `roles` list as
519
585
  // the all-roles fallback).
520
- const collectives = new Map<string, { name: string; kind: 'role' | 'group' }>();
586
+ const collectives = new Map<string, ResolvedPrincipal>();
521
587
  for (const list of [eligible, readers, owners, downloaders]) {
522
588
  const kinded =
523
589
  list.principals ?? list.roles.map((name) => ({ name, kind: 'role' as const }));
524
590
  for (const p of kinded) {
525
- const key = `${p.kind === 'group' ? 'g' : 'r'}:${p.name.toLowerCase()}`;
591
+ const key = `${p.kind === 'group' ? 'g' : p.kind === 'plugin' ? 'p' : 'r'}:${p.name.toLowerCase()}`;
526
592
  if (!collectives.has(key)) collectives.set(key, p);
527
593
  }
528
594
  }
@@ -673,6 +739,22 @@ export function createAccessRoutes(
673
739
  { kind: 'unknown-group', group: principal.group },
674
740
  );
675
741
  }
742
+ } else if (principal.kind === 'plugin') {
743
+ // A plugin grant must name a plugin the caller can DISCOVER on the
744
+ // default branch — the same set the suggest list offers, so a plugin
745
+ // hidden from the picker is refused here with the same words as one
746
+ // that does not exist. Matched on the manifest slug, the identity the
747
+ // token canonicalises to.
748
+ const { plugins } = await defaultBranchPrincipals();
749
+ const slug = pluginManifestName(principal.plugin);
750
+ const visible = await discoverablePlugins(user.email, plugins);
751
+ if (!visible.some((p) => pluginManifestName(p.name) === slug)) {
752
+ throw new AccessMutationError(
753
+ `No plugin named "${principal.plugin}". Pick an existing plugin.`,
754
+ 404,
755
+ { kind: 'unknown-plugin', plugin: principal.plugin },
756
+ );
757
+ }
676
758
  }
677
759
  // Splice shape: group → bare token (group-first precedence); role →
678
760
  // explicit `role/<Name>` token (see asSplicePrincipal).
@@ -5,6 +5,7 @@ import {
5
5
  registerAccessFrontmatterExtensions,
6
6
  accessFrontmatterExtensionList,
7
7
  parseOwnAccessEntries,
8
+ parseRolesYaml,
8
9
  } from '../access-grammar.js';
9
10
  import { parseGroupsFile } from '../group-files.js';
10
11
 
@@ -22,6 +23,23 @@ describe('parseYamlSubset — inline empty collections', () => {
22
23
  });
23
24
  });
24
25
 
26
+ describe("the reserved 'plugin/' prefix — no role or group may pose as a plugin principal", () => {
27
+ it('parseGroupsFile skips such a group with a warning and keeps the rest', () => {
28
+ const res = parseGroupsFile('groups:\n plugin/GTM/read:\n - mallory@x.io\n Sales:\n - sam@x.io\n', 'groups.yaml');
29
+ expect(res.ok).toBe(true);
30
+ if (!res.ok) return;
31
+ expect([...res.groups.keys()]).toEqual(['sales']);
32
+ expect(res.warnings.some((w) => w.includes("reserved 'plugin/'"))).toBe(true);
33
+ });
34
+
35
+ it('parseRolesYaml refuses such a role outright', () => {
36
+ const res = parseRolesYaml('roles:\n Admin:\n - admin@x.io\n plugin/GTM/read:\n - mallory@x.io\n');
37
+ expect(res.ok).toBe(false);
38
+ if (res.ok) return;
39
+ expect(res.errors.some((e) => e.includes("reserved 'plugin/'"))).toBe(true);
40
+ });
41
+ });
42
+
25
43
  describe('parseGroupsFile — empty group sources', () => {
26
44
  it('accepts a bare `groups:` as a valid empty set', () => {
27
45
  const res = parseGroupsFile('groups:\n', 'groups.yaml');
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ canonicalPluginToken,
4
+ canonicalRoleName,
5
+ parseAccessEntry,
6
+ parsePluginPrincipalKey,
7
+ } from '../access-grammar.js';
8
+
9
+ /**
10
+ * ONE spelling for a plugin principal. Every spelling of a token folds to the
11
+ * same canonical key — through the parser, through the role canonicaliser
12
+ * that grants and revocations compare with — and a canonical key is exactly
13
+ * one name and one verb: nothing nested, nothing that is not already a slug.
14
+ */
15
+ describe('plugin principal tokens — one canonical form', () => {
16
+ it('folds any spelling of a plugin token to its slugged key', () => {
17
+ for (const spelling of ['plugin/Sales Team/read', 'plugin/sales-team/read', 'Plugin/SALES TEAM/READ', ' plugin/Sales Team/read ']) {
18
+ expect(canonicalPluginToken(spelling)).toBe('plugin/sales-team/read');
19
+ expect(canonicalRoleName(spelling)).toBe('plugin/sales-team/read');
20
+ }
21
+ const parsed = parseAccessEntry('plugin/Sales Team/write');
22
+ expect(parsed.ok && parsed.entry.kind === 'role' ? parsed.entry.role : null).toBe('plugin/sales-team/write');
23
+ });
24
+
25
+ it('is not a plugin token without a name, with a nested name, or with an unknown verb', () => {
26
+ for (const bad of ['plugin//read', 'plugin/GTM', 'plugin/GTM/foo/read', 'plugin/GTM/delete', 'role/GTM/read']) {
27
+ expect(canonicalPluginToken(bad)).toBeNull();
28
+ }
29
+ // A non-plugin role still canonicalises the old way.
30
+ expect(canonicalRoleName(' Sales Team ')).toBe('sales team');
31
+ });
32
+
33
+ it('accepts as a canonical key only what it would itself generate', () => {
34
+ expect(parsePluginPrincipalKey('plugin/gtm/read')).toEqual({ slug: 'gtm', verb: 'read' });
35
+ expect(parsePluginPrincipalKey('plugin/gtm/foo/read')).toBeNull();
36
+ expect(parsePluginPrincipalKey('plugin/GTM/read')).toBeNull(); // not a slug
37
+ expect(parsePluginPrincipalKey('plugin/gtm/delete')).toBeNull();
38
+ expect(parsePluginPrincipalKey('plugin//read')).toBeNull();
39
+ });
40
+ });
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { parse as parseFullYaml } from 'yaml';
15
+ import { pluginManifestName } from '@bevel-software/platform-shared';
15
16
  import type { GroupsIndex } from './group-files.js';
16
17
 
17
18
  // ---------------------------------------------------------------------------
@@ -86,6 +87,73 @@ export const GROUP_REF_PREFIX = 'group:';
86
87
  */
87
88
  export const ROLE_TOKEN_PREFIX = 'role/';
88
89
 
90
+ /**
91
+ * PLUGIN-principal token prefix in access.md entries: `plugin/<Name>/<verb>`
92
+ * names everyone who holds `<verb>` on the plugin folder `Plugins/<Name>` —
93
+ * `plugin/GTM/read` is the GTM plugin's members, `plugin/GTM/write` its
94
+ * managers. Membership is DERIVED at model-load time from the plugin's own
95
+ * `access.md` (see `plugin-principals.ts`), so a grant naming one follows the
96
+ * plugin's roster with no copying. This is how a shared skill under `Skills/`
97
+ * is made visible to a plugin: `read: plugin/GTM/read` on the skill folder.
98
+ *
99
+ * The name half is canonicalised with the plugin's MANIFEST slug, so
100
+ * `plugin/Sales Team/read` and `plugin/sales-team/read` are one principal —
101
+ * the same identity a conformant client keys the plugin on. Reserved in the
102
+ * group name-safety rules like `role/`.
103
+ */
104
+ export const PLUGIN_TOKEN_PREFIX = 'plugin/';
105
+
106
+ /** The verbs a plugin token may name — each is a distinct principal. */
107
+ export const PLUGIN_TOKEN_VERBS = ['read', 'write', 'owner'] as const;
108
+ export type PluginTokenVerb = (typeof PLUGIN_TOKEN_VERBS)[number];
109
+
110
+ /** The canonical key of a plugin principal, from the plugin's manifest slug. */
111
+ export function pluginPrincipalKey(slug: string, verb: PluginTokenVerb): string {
112
+ return `${PLUGIN_TOKEN_PREFIX}${slug}/${verb}`;
113
+ }
114
+
115
+ /**
116
+ * The parts of a CANONICAL plugin key, or null when the text is not one:
117
+ * exactly one separator after the prefix, a slug that is its own manifest
118
+ * slug, one of the three verbs. A generated key never carries a nested
119
+ * name, so `plugin/a/b/read` is a typo, not a principal.
120
+ */
121
+ export function parsePluginPrincipalKey(
122
+ canonical: string,
123
+ ): { slug: string; verb: PluginTokenVerb } | null {
124
+ if (!canonical.startsWith(PLUGIN_TOKEN_PREFIX)) return null;
125
+ const rest = canonical.slice(PLUGIN_TOKEN_PREFIX.length);
126
+ const cut = rest.indexOf('/');
127
+ if (cut <= 0 || rest.indexOf('/', cut + 1) !== -1) return null;
128
+ const slug = rest.slice(0, cut);
129
+ if (pluginManifestName(slug) !== slug) return null;
130
+ const verb = rest.slice(cut + 1);
131
+ if (!(PLUGIN_TOKEN_VERBS as readonly string[]).includes(verb)) return null;
132
+ return { slug, verb: verb as PluginTokenVerb };
133
+ }
134
+
135
+ /**
136
+ * The canonical key for ANY spelling of a plugin token — `plugin/Sales Team/read`,
137
+ * `Plugin/sales-team/READ` — or null when the text is not one: a name must be
138
+ * present and hold no further `/`, the verb must be one of the three. The ONE
139
+ * place a plugin's spelling folds to its slug: the access-file parser, the
140
+ * role canonicaliser (which grant and revoke compare through) and the link
141
+ * service all go through it, so a grant written one way is found again
142
+ * however it was spelled.
143
+ */
144
+ export function canonicalPluginToken(token: string): string | null {
145
+ const body = token.trim();
146
+ if (!body.toLowerCase().startsWith(PLUGIN_TOKEN_PREFIX)) return null;
147
+ const rest = body.slice(PLUGIN_TOKEN_PREFIX.length).trim();
148
+ const cut = rest.lastIndexOf('/');
149
+ const name = cut > 0 ? rest.slice(0, cut).trim() : '';
150
+ const verb = cut > 0 ? rest.slice(cut + 1).trim().toLowerCase() : '';
151
+ if (!name || name.includes('/') || !(PLUGIN_TOKEN_VERBS as readonly string[]).includes(verb)) return null;
152
+ const slug = pluginManifestName(name);
153
+ if (!slug) return null;
154
+ return pluginPrincipalKey(slug, verb as PluginTokenVerb);
155
+ }
156
+
89
157
  export const USER_REF_REGEX = /^(.+?)\s+<\s*([^<>\s]+@[^<>\s]+)\s*>\s*$/;
90
158
  export const EMAIL_REGEX = /^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/;
91
159
 
@@ -224,9 +292,24 @@ export function hasAccessFrontmatterExtension(p: string): boolean {
224
292
  export interface RolesIndex {
225
293
  byCanonical: Map<
226
294
  string,
227
- { displayName: string; emails: Set<string>; groupRefs?: Set<string>; kind?: 'role' | 'group' }
295
+ {
296
+ displayName: string;
297
+ emails: Set<string>;
298
+ groupRefs?: Set<string>;
299
+ kind?: 'role' | 'group' | 'plugin';
300
+ /** For `kind: 'plugin'`: the plugin's identity (its manifest name) the principal derives from. */
301
+ pluginName?: string;
302
+ /** For `kind: 'plugin'`: the repo-relative plugin directory, e.g. `Plugins/GTM`. */
303
+ pluginDir?: string;
304
+ }
228
305
  >;
229
306
  byEmail: Map<string, Set<string>>;
307
+ /**
308
+ * Principal keys EVERY caller holds, known or not — a plugin whose own
309
+ * rules grant `everyone` yields a plugin principal no email list can
310
+ * enumerate. The resolver unions these into every caller's key set.
311
+ */
312
+ publicKeys?: Set<string>;
230
313
  }
231
314
  // ---------------------------------------------------------------------------
232
315
  // Tiny YAML subset parser — handles only block mappings + block sequences
@@ -429,7 +512,9 @@ export function bodyAfterFrontmatter(text: string): string {
429
512
  // ---------------------------------------------------------------------------
430
513
 
431
514
  export function canonicalRoleName(name: string): string {
432
- return name.trim().toLowerCase().replace(/\s+/g, ' ');
515
+ // A plugin token canonicalises to its KEY (slugged name), not to its
516
+ // lowercased spelling — see `canonicalPluginToken`.
517
+ return canonicalPluginToken(name) ?? name.trim().toLowerCase().replace(/\s+/g, ' ');
433
518
  }
434
519
 
435
520
  export function canonicalEmail(email: string): string {
@@ -482,6 +567,23 @@ export function parseAccessEntry(
482
567
  const suffix = canonicalRoleName(role.slice(ROLE_TOKEN_PREFIX.length));
483
568
  if (!suffix) return { ok: false, error: `entry '${body}' names no role after '${ROLE_TOKEN_PREFIX}'` };
484
569
  role = `${ROLE_TOKEN_PREFIX}${suffix}`;
570
+ } else if (role.startsWith(PLUGIN_TOKEN_PREFIX)) {
571
+ // `plugin/<Name>/<verb>`: the name is canonicalised to the plugin's
572
+ // manifest slug, the verb must be one of the three. Anything else is a
573
+ // parse error naming the valid shapes — a token that silently resolved to
574
+ // nothing would be a grant nobody gets and nobody is told about.
575
+ const key = canonicalPluginToken(body);
576
+ // Exactly one separator: `plugin/GTM/foo/read` is a typo, not a grant to
577
+ // some plugin whose slug happens to fold `GTM/foo` into `gtm-foo`.
578
+ if (key === null) {
579
+ return {
580
+ ok: false,
581
+ error:
582
+ `entry '${body}' is not a plugin principal — write it as ` +
583
+ `${PLUGIN_TOKEN_PREFIX}<plugin>/read, ${PLUGIN_TOKEN_PREFIX}<plugin>/write or ${PLUGIN_TOKEN_PREFIX}<plugin>/owner`,
584
+ };
585
+ }
586
+ role = key;
485
587
  }
486
588
  return { ok: true, entry: { kind: 'role', role, displayRole: body, deny } };
487
589
  }
@@ -529,6 +631,16 @@ export function parseRolesYaml(
529
631
  );
530
632
  continue;
531
633
  }
634
+ // A role spelled `plugin/…` would be overwritten by the synthesised plugin
635
+ // principal of the same key while its members kept the token — plugin
636
+ // access for people who are not plugin members. Refused at parse time,
637
+ // like the group name-safety rule.
638
+ if (canonical.startsWith(PLUGIN_TOKEN_PREFIX)) {
639
+ errors.push(
640
+ `roles.yaml: role '${displayName}' starts with the reserved '${PLUGIN_TOKEN_PREFIX}' prefix — that spelling is the plugin-principal token in access entries`,
641
+ );
642
+ continue;
643
+ }
532
644
  if (index.byCanonical.has(canonical)) {
533
645
  const prev = index.byCanonical.get(canonical)!.displayName;
534
646
  errors.push(
@@ -2,6 +2,7 @@ import {
2
2
  EMAIL_REGEX,
3
3
  GROUP_REF_PREFIX,
4
4
  RESERVED_ROLE_NAMES,
5
+ PLUGIN_TOKEN_PREFIX,
5
6
  ROLE_TOKEN_PREFIX,
6
7
  canonicalEmail,
7
8
  canonicalRoleName,
@@ -68,6 +69,9 @@ export function unsafeNameReason(displayName: string): string | null {
68
69
  if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
69
70
  return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
70
71
  }
72
+ if (canonicalRoleName(trimmed).startsWith(PLUGIN_TOKEN_PREFIX)) {
73
+ return `starts with the reserved '${PLUGIN_TOKEN_PREFIX}' prefix (the plugin-principal token in access entries)`;
74
+ }
71
75
  return null;
72
76
  }
73
77
 
@@ -0,0 +1,137 @@
1
+ import { comparePathComponents } from '../../shared/path-order.js';
2
+ import {
3
+ EVERYONE_CANONICAL,
4
+ PLUGIN_TOKEN_PREFIX,
5
+ PLUGIN_TOKEN_VERBS,
6
+ pluginPrincipalKey,
7
+ type AccessFile,
8
+ type PluginTokenVerb,
9
+ type RolesIndex,
10
+ type Verb,
11
+ } from './access-grammar.js';
12
+
13
+ /**
14
+ * Plugin principals — `plugin/<Name>/<verb>` — synthesised into the principal
15
+ * index from each plugin folder's own `access.md`, so a grant naming one
16
+ * resolves exactly like a group grant: the resolver sees a named principal
17
+ * with member emails and needs no plugin awareness at all.
18
+ *
19
+ * WHAT A PLUGIN'S ROSTER IS: the folder-governing rules of
20
+ * `Plugins/<Name>/access.md` — the file provisioning seeds and the join flow
21
+ * edits. Three principals per plugin, following the resolver's own superset
22
+ * convention (write implies read, owner implies both):
23
+ *
24
+ * plugin/<slug>/read everyone under read, write or owner
25
+ * plugin/<slug>/write everyone under write or owner
26
+ * plugin/<slug>/owner everyone under owner
27
+ *
28
+ * The `<slug>` is the manifest slug of the folder (`pluginManifestName`), the
29
+ * identity `parseAccessEntry` canonicalises a hand-written token to.
30
+ *
31
+ * Deliberately NOT membership:
32
+ * - the file's frontmatter (`read: everyone` there is DISCOVERABILITY of
33
+ * the access.md itself, not membership) — `AccessFile.entries` already
34
+ * holds only the folder block;
35
+ * - rules inherited from above the plugin folder (the repo root's
36
+ * `write: Admin`) — admins reach the skill through those same rules
37
+ * anyway, and the roster should be what the plugin's page shows;
38
+ * - `deny` entries — a denial subtracts nothing here; a plugin that wants
39
+ * someone out removes the grant;
40
+ * - other plugin tokens inside a plugin's rules — skipped, so synthesis is
41
+ * one pass with no recursion and no cycle to detect.
42
+ *
43
+ * `everyone` in a plugin's own rules makes the corresponding principal
44
+ * PUBLIC: no email list can enumerate it, so its key is recorded in
45
+ * `index.publicKeys` and the resolver unions those into every caller's set.
46
+ *
47
+ * Runs AFTER groups are merged (so role and group entries expand through the
48
+ * finished index) and BEFORE unknown-role validation (so plugin tokens in
49
+ * access files count as known).
50
+ */
51
+ export function synthesizePluginPrincipals(
52
+ index: RolesIndex,
53
+ accessFiles: ReadonlyMap<string, AccessFile>,
54
+ /**
55
+ * The repo-relative folders that ARE plugins — the ones carrying a
56
+ * `plugin.json` — each with its IDENTITY (the manifest's name, or the
57
+ * folder folded into one), as the model loader read them. A plugin is a
58
+ * folder with a manifest, at any depth; its access.md is its roster.
59
+ * Nothing else's is.
60
+ */
61
+ pluginDirs: ReadonlyMap<string, string>,
62
+ ): void {
63
+ const claimed = new Set<string>();
64
+ // The same path order discovery walks in, so "first by path" is the same
65
+ // plugin in both places — see `comparePathComponents`.
66
+ for (const [dir, slug] of [...pluginDirs].sort(([a], [b]) => comparePathComponents(a, b))) {
67
+ // Two plugins with one name: discovery keeps the first by path, so do we —
68
+ // and the claim is made BEFORE looking for a roster, or a first plugin
69
+ // without rules would leave its slug to a later twin's roster.
70
+ if (claimed.has(slug)) continue;
71
+ claimed.add(slug);
72
+ const file = accessFiles.get(dir);
73
+ if (!file) continue; // a plugin without rules has no roster
74
+ for (const verb of PLUGIN_TOKEN_VERBS) {
75
+ const { emails, everyone } = holdersOf(index, file, verb);
76
+ const key = pluginPrincipalKey(slug, verb);
77
+ index.byCanonical.set(key, {
78
+ displayName: key,
79
+ emails,
80
+ kind: 'plugin',
81
+ pluginName: slug,
82
+ pluginDir: dir,
83
+ });
84
+ for (const email of emails) {
85
+ let set = index.byEmail.get(email);
86
+ if (!set) {
87
+ set = new Set();
88
+ index.byEmail.set(email, set);
89
+ }
90
+ set.add(key);
91
+ }
92
+ if (everyone) {
93
+ index.publicKeys ??= new Set();
94
+ index.publicKeys.add(key);
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ /** The verbs whose grants confer `verb` on the plugin (the superset fold). */
101
+ function sourceVerbsFor(verb: PluginTokenVerb): Verb[] {
102
+ switch (verb) {
103
+ case 'read':
104
+ return ['read', 'write', 'owner'];
105
+ case 'write':
106
+ return ['write', 'owner'];
107
+ case 'owner':
108
+ return ['owner'];
109
+ }
110
+ }
111
+
112
+ function holdersOf(
113
+ index: RolesIndex,
114
+ file: AccessFile,
115
+ verb: PluginTokenVerb,
116
+ ): { emails: Set<string>; everyone: boolean } {
117
+ const emails = new Set<string>();
118
+ let everyone = false;
119
+ for (const source of sourceVerbsFor(verb)) {
120
+ for (const entry of file.entries[source]) {
121
+ if (entry.deny) continue;
122
+ if (entry.kind === 'user') {
123
+ emails.add(entry.email);
124
+ continue;
125
+ }
126
+ if (entry.role === EVERYONE_CANONICAL) {
127
+ everyone = true;
128
+ continue;
129
+ }
130
+ if (entry.role.startsWith(PLUGIN_TOKEN_PREFIX)) continue; // no recursion
131
+ const record = index.byCanonical.get(entry.role);
132
+ if (!record) continue; // an unknown role contributes nothing (loadModel warns separately)
133
+ for (const email of record.emails) emails.add(email);
134
+ }
135
+ }
136
+ return { emails, everyone };
137
+ }
@@ -183,6 +183,12 @@ export const externalApiKeys = pgTable('api_tokens', {
183
183
  userId: uuid('user_id').notNull().references(() => users.id),
184
184
  tokenHash: text('token_hash').notNull().unique(),
185
185
  label: text('label').notNull(),
186
+ /**
187
+ * What the key was minted as: `key` by hand, or a flow's own kind (a
188
+ * Claude link). The one fact that tells such keys apart — the label is
189
+ * free text the person may edit or imitate.
190
+ */
191
+ kind: text('kind').default('key').notNull(),
186
192
  createdAt: timestamp('created_at').defaultNow().notNull(),
187
193
  lastUsedAt: timestamp('last_used_at'),
188
194
  revokedAt: timestamp('revoked_at'),
@@ -490,3 +496,51 @@ export const deploymentSettings = pgTable('deployment_settings', {
490
496
  updatedBy: uuid('updated_by').references(() => users.id, { onDelete: 'set null' }),
491
497
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
492
498
  });
499
+
500
+ /**
501
+ * What this deployment presents to claude.ai as a "GitHub Enterprise Server"
502
+ * so that Cowork and claude.ai can add the per-user marketplace: the app id,
503
+ * client id and secrets an Owner pastes into Claude's admin settings. ONE row,
504
+ * generated on first use, replaced whole on rotate. Secrets are sealed with
505
+ * the secrets key, as stored settings are — see
506
+ * `marketplace/github-facade/github-facade-credentials.service.ts`.
507
+ */
508
+ export const githubFacadeIdentity = pgTable('github_facade_identity', {
509
+ id: text('id').primaryKey(),
510
+ appId: text('app_id').notNull(),
511
+ clientId: text('client_id').notNull(),
512
+ /** Sealed. */
513
+ clientSecret: text('client_secret').notNull(),
514
+ /** Sealed. */
515
+ webhookSecret: text('webhook_secret').notNull(),
516
+ /** Sealed — PKCS#1 PEM. */
517
+ privateKeyPem: text('private_key_pem').notNull(),
518
+ publicKeyPem: text('public_key_pem').notNull(),
519
+ createdAt: timestamp('created_at').defaultNow().notNull(),
520
+ rotatedAt: timestamp('rotated_at'),
521
+ });
522
+
523
+ /**
524
+ * One-time codes the Claude connect flow issues on the consent page and
525
+ * Anthropic's backend exchanges seconds later — in the database so the
526
+ * replica that issued a code and the replica asked to exchange it agree.
527
+ *
528
+ * Keyed by the PERSON: "one live code per person" is the table's own rule,
529
+ * not a cleanup's. Issuing upserts their row — the newest code overwrites
530
+ * the last in one statement — so the table holds at most one row per user,
531
+ * for as long as the user exists (the row goes with them), and nothing ever
532
+ * has to sweep it. The client id is data the exchange checks, not part of
533
+ * the key: the bridge has one client, and a rotated client id must not leave
534
+ * a dead row behind under the old one. The code's hash is the unique lookup
535
+ * an exchange uses, spent by a conditional update. See
536
+ * `marketplace/github-facade/github-facade-codes.store.ts`.
537
+ */
538
+ export const githubFacadeCodes = pgTable('github_facade_codes', {
539
+ userId: uuid('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
540
+ clientId: text('client_id').notNull(),
541
+ codeHash: text('code_hash').notNull().unique(),
542
+ redirectUri: text('redirect_uri').notNull(),
543
+ expiresAt: timestamp('expires_at').notNull(),
544
+ consumedAt: timestamp('consumed_at'),
545
+ createdAt: timestamp('created_at').defaultNow().notNull(),
546
+ });