@bevel-software/platform-core-backend 0.7.5 → 0.9.0

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 (282) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +8 -1
  4. package/dist/core/core-ports.d.ts.map +1 -1
  5. package/dist/core/core-ports.js.map +1 -1
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +75 -18
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +12 -5
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +69 -36
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +8 -7
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +1 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +6 -6
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access-declarations.d.ts +5 -5
  20. package/dist/modules/access/access-declarations.js +3 -3
  21. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  22. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-mutation.service.js +3 -3
  24. package/dist/modules/access/access-mutation.service.js.map +1 -1
  25. package/dist/modules/access/access-splice.js +4 -4
  26. package/dist/modules/access/access-splice.js.map +1 -1
  27. package/dist/modules/access/access.routes.d.ts +3 -1
  28. package/dist/modules/access/access.routes.d.ts.map +1 -1
  29. package/dist/modules/access/access.routes.js +22 -20
  30. package/dist/modules/access/access.routes.js.map +1 -1
  31. package/dist/modules/access/creator-access.d.ts +2 -2
  32. package/dist/modules/access/creator-access.js +5 -5
  33. package/dist/modules/access/creator-access.js.map +1 -1
  34. package/dist/modules/access/render-roles-yaml.d.ts +22 -0
  35. package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
  36. package/dist/modules/access/render-roles-yaml.js +56 -0
  37. package/dist/modules/access/render-roles-yaml.js.map +1 -0
  38. package/dist/modules/access/roles-admin.service.d.ts +33 -5
  39. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  40. package/dist/modules/access/roles-admin.service.js +44 -19
  41. package/dist/modules/access/roles-admin.service.js.map +1 -1
  42. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  43. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  44. package/dist/modules/code-mode/code-mode-names.js +5 -27
  45. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  46. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  47. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  48. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  49. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  50. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  51. package/dist/modules/mcp/mcp.service.js +33 -395
  52. package/dist/modules/mcp/mcp.service.js.map +1 -1
  53. package/dist/modules/plugins/index.d.ts +7 -0
  54. package/dist/modules/plugins/index.d.ts.map +1 -0
  55. package/dist/modules/plugins/index.js +6 -0
  56. package/dist/modules/plugins/index.js.map +1 -0
  57. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  58. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  59. package/dist/modules/plugins/join-proposals.js +67 -0
  60. package/dist/modules/plugins/join-proposals.js.map +1 -0
  61. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  62. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  63. package/dist/modules/plugins/join-requests.service.js +135 -0
  64. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  65. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  66. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  68. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  69. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  70. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.contract.js +36 -0
  72. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  73. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  74. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  75. package/dist/modules/plugins/plugins.routes.js +379 -0
  76. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  77. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  78. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  79. package/dist/modules/plugins/plugins.service.js +172 -0
  80. package/dist/modules/plugins/plugins.service.js.map +1 -0
  81. package/dist/modules/settings/setup.routes.d.ts +10 -1
  82. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  83. package/dist/modules/settings/setup.routes.js +111 -6
  84. package/dist/modules/settings/setup.routes.js.map +1 -1
  85. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  86. package/dist/modules/skills/pending-skills.service.js +7 -7
  87. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  88. package/dist/modules/skills/skills.contract.d.ts +4 -4
  89. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  90. package/dist/modules/skills/skills.contract.js +1 -1
  91. package/dist/modules/skills/skills.service.js +5 -5
  92. package/dist/modules/skills/skills.service.js.map +1 -1
  93. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  94. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  95. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  96. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  97. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  98. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  99. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  100. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  101. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  102. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  103. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  104. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  105. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  106. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  107. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  108. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  109. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  110. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  111. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  112. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  113. package/dist/modules/workflow/git/git.service.js +2 -2
  114. package/dist/modules/workflow/git/git.service.js.map +1 -1
  115. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  116. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  117. package/dist/modules/workspace/kb-seed.service.js +43 -10
  118. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  119. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  120. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  121. package/dist/modules/workspace/plugins-migration.js +379 -0
  122. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  123. package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
  124. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
  125. package/dist/modules/workspace/startup/kb-git.js +86 -0
  126. package/dist/modules/workspace/startup/kb-git.js.map +1 -0
  127. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
  128. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
  129. package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
  130. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
  131. package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
  132. package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
  133. package/dist/modules/workspace/startup/on-server-start.js +21 -0
  134. package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
  135. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
  136. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
  137. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
  138. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
  139. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
  140. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
  141. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
  142. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
  143. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
  144. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
  145. package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
  146. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
  147. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
  148. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
  149. package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
  150. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
  151. package/dist/modules/workspace/workspace.routes.js +3 -3
  152. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  153. package/dist/modules/workspace/workspace.service.d.ts +0 -9
  154. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  155. package/dist/modules/workspace/workspace.service.js +2 -30
  156. package/dist/modules/workspace/workspace.service.js.map +1 -1
  157. package/dist/shared/kb-layout.test.js +3 -3
  158. package/dist/shared/kb-layout.test.js.map +1 -1
  159. package/dist/shared/utcp-namespace.d.ts +6 -27
  160. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  161. package/dist/shared/utcp-namespace.js +6 -63
  162. package/dist/shared/utcp-namespace.js.map +1 -1
  163. package/dist/shared/variable-refs.d.ts +42 -0
  164. package/dist/shared/variable-refs.d.ts.map +1 -0
  165. package/dist/shared/variable-refs.js +60 -0
  166. package/dist/shared/variable-refs.js.map +1 -0
  167. package/kb-template/.bevelignore +1 -1
  168. package/kb-template/AGENTS.md +88 -35
  169. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  170. package/kb-template/access.md +36 -36
  171. package/kb-template/gitignore.template +16 -0
  172. package/migrations/meta/0000_snapshot.json +1479 -1479
  173. package/package.json +5 -4
  174. package/src/assets.ts +25 -25
  175. package/src/core/core-ports.ts +113 -106
  176. package/src/core/create-core-server.ts +85 -20
  177. package/src/core/create-core-services.ts +83 -42
  178. package/src/index.ts +69 -69
  179. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  180. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  181. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  182. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  183. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  184. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  185. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  186. package/src/modules/access/__tests__/roles-admin.service.test.ts +36 -2
  187. package/src/modules/access/access-control.interface.ts +8 -7
  188. package/src/modules/access/access-control.service.ts +7 -7
  189. package/src/modules/access/access-declarations.ts +5 -5
  190. package/src/modules/access/access-mutation.service.ts +3 -3
  191. package/src/modules/access/access-splice.ts +4 -4
  192. package/src/modules/access/access.routes.ts +23 -20
  193. package/src/modules/access/creator-access.ts +5 -5
  194. package/src/modules/access/render-roles-yaml.ts +65 -0
  195. package/src/modules/access/roles-admin.service.ts +45 -16
  196. package/src/modules/admin/admin-access.routes.ts +29 -29
  197. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  198. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  199. package/src/modules/auth/rate-limit.ts +45 -45
  200. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  201. package/src/modules/code-mode/code-mode-names.ts +10 -36
  202. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  203. package/src/modules/database/connection.ts +15 -15
  204. package/src/modules/database/schema.ts +11 -11
  205. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  206. package/src/modules/mcp/mcp.service.ts +57 -435
  207. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  208. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  209. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  210. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  211. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  212. package/src/modules/plugins/index.ts +17 -0
  213. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  214. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  215. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  216. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  217. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  218. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  219. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  220. package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
  221. package/src/modules/settings/setup.routes.ts +112 -5
  222. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  223. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  224. package/src/modules/skills/pending-skills.service.ts +7 -7
  225. package/src/modules/skills/skills.contract.ts +4 -4
  226. package/src/modules/skills/skills.service.ts +5 -5
  227. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  228. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  229. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  230. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  231. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  232. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  233. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  234. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  235. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  236. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  237. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  238. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  239. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  240. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  241. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  242. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  243. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  244. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  245. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  246. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  247. package/src/modules/workflow/git/git.service.ts +2 -2
  248. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  249. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  250. package/src/modules/workflow/workflow-hooks.ts +101 -101
  251. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  252. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  253. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  254. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  255. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  256. package/src/modules/workspace/__tests__/workspace.service.test.ts +27 -0
  257. package/src/modules/workspace/bevel-ignore.ts +66 -66
  258. package/src/modules/workspace/session-sink.ts +25 -25
  259. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
  260. package/src/modules/workspace/startup/kb-git.ts +94 -0
  261. package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
  262. package/src/modules/workspace/startup/on-server-start.ts +97 -0
  263. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
  264. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
  265. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
  266. package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
  267. package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
  268. package/src/modules/workspace/workspace.routes.ts +3 -3
  269. package/src/modules/workspace/workspace.service.ts +2 -35
  270. package/src/modules/workspace/workspace.tools.ts +922 -922
  271. package/src/shared/__tests__/join-request.test.ts +13 -13
  272. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  273. package/src/shared/kb-layout.test.ts +3 -3
  274. package/src/shared/utcp-namespace.ts +10 -68
  275. package/src/shared/variable-refs.ts +64 -0
  276. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  277. package/src/modules/groups/index.ts +0 -17
  278. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
  279. package/src/modules/workspace/kb-seed.interface.ts +0 -36
  280. package/src/modules/workspace/kb-seed.service.ts +0 -555
  281. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  282. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  configureBranchModel,
6
6
  validateBranchModel,
7
7
  PROTECTED_BRANCHES,
8
- GROUPS_DIR,
8
+ PLUGINS_DIR,
9
9
  } from '@bevel-software/platform-shared';
10
10
  import { CoreConfig } from '../core-config.js';
11
11
  import { getDb, type Database } from '../modules/database/connection.js';
@@ -13,7 +13,11 @@ import { runCoreMigrations } from '../modules/database/migrate.js';
13
13
  import { coreMigrationsDir } from '../assets.js';
14
14
  import { WorkspaceService } from '../modules/workspace/workspace.service.js';
15
15
  import { RoutineWritePolicyService } from '../modules/workspace/routine-write-policy.js';
16
- import { KbSeedService } from '../modules/workspace/kb-seed.service.js';
16
+ import { KbStartupRunner } from '../modules/workspace/startup/kb-startup-runner.js';
17
+ import { GroupsToPluginsStep } from '../modules/workspace/startup/steps/groups-to-plugins.step.js';
18
+ import { TemplateFilesStep } from '../modules/workspace/startup/steps/template-files.step.js';
19
+ import { RolesYamlStep } from '../modules/workspace/startup/steps/roles-yaml.step.js';
20
+ import { buildSeedTree } from '../modules/workspace/startup/steps/seed-tree.js';
17
21
  import { DeploymentSettingsService } from '../modules/settings/deployment-settings.service.js';
18
22
 
19
23
  /** `a.com, b.com` → `['a.com','b.com']`, tolerating a leading `@` or `.`. */
@@ -33,7 +37,8 @@ import { AccessControlService } from '../modules/access/access-control.service.j
33
37
  import { CreatorAccessService } from '../modules/access/creator-access.js';
34
38
  import { PendingSkillsService, SkillService } from '../modules/skills/index.js';
35
39
  import { ToolManualService } from '../modules/tool-manuals/index.js';
36
- import { GroupIndexService, GroupProvisionService, JoinRequestsService } from '../modules/groups/index.js';
40
+ import { McpServerEditService } from '../modules/tool-manuals/mcp-server-edit.service.js';
41
+ import { PluginIndexService, PluginProvisionService, JoinRequestsService } from '../modules/plugins/index.js';
37
42
  import {
38
43
  DbSecretsVaultService,
39
44
  McpOAuthDiscoveryService,
@@ -93,7 +98,12 @@ export interface CoreServices {
93
98
  config: CoreConfig;
94
99
  db: Database;
95
100
  workspaceService: WorkspaceService;
96
- kbSeedService: KbSeedService;
101
+ /**
102
+ * The KB startup phase (see `startup/on-server-start.ts`): run at the
103
+ * deployment's two quiet moments — boot, before routes mount, and
104
+ * first-time setup completion — and never again while the process serves.
105
+ */
106
+ kbStartupRunner: KbStartupRunner;
97
107
  spillStore: SpillStore;
98
108
  accessControl: AccessControlService;
99
109
  creatorAccess: CreatorAccessService;
@@ -102,8 +112,9 @@ export interface CoreServices {
102
112
  skillService: SkillService;
103
113
  pendingSkillsService: PendingSkillsService;
104
114
  toolManualService: ToolManualService;
105
- groupIndexService: GroupIndexService;
106
- groupProvisionService: GroupProvisionService;
115
+ mcpServerEditService: McpServerEditService;
116
+ pluginIndexService: PluginIndexService;
117
+ pluginProvisionService: PluginProvisionService;
107
118
  joinRequestsService: JoinRequestsService;
108
119
  authService: AuthService;
109
120
  authMiddleware: ReturnType<typeof createAuthMiddleware>;
@@ -211,27 +222,39 @@ export async function createCoreServices(
211
222
  kbDirName,
212
223
  () => settings.resolve('gitUsername') || 'x-access-token',
213
224
  );
214
- // Seed (or top-up) the KB remote before the first clone so the rest of the app
215
- // can keep assuming the remote already carries the protected branches + base
216
- // scaffolding — a fresh or partially-populated remote no longer needs to have
217
- // been forked from the standalone template.
218
- const kbSeedService = new KbSeedService(
219
- () => settings.resolve('kbRepoUrl'),
220
- config.kbTemplateDir,
221
- // Getters, not values: the branch model can arrive from the setup screen
222
- // after this object exists, and seeding is the first thing that needs it.
223
- () => [...PROTECTED_BRANCHES],
224
- () => DEFAULT_BRANCH,
225
+ // The KB startup phase: every seeding, scaffolding and migration concern,
226
+ // run through one runner at the deployment's quiet moments (boot + setup
227
+ // completion) instead of lazily on branch loads. Root folders this
228
+ // distribution reserves on top of core's two are a plain value, not a
229
+ // getter: they are named in the composition root rather than collected on
230
+ // the setup screen, so there is nothing to re-read.
231
+ const extraDirs = ports.kbExtraRootDirs ?? [];
232
+ // Core's steps first, in dependency order — the Groups→Plugins migration
233
+ // reshapes trees the template top-up would otherwise re-scaffold — then
234
+ // whatever the distribution appends.
235
+ const kbStartupSteps = [
236
+ new GroupsToPluginsStep(),
237
+ new TemplateFilesStep(extraDirs),
238
+ new RolesYamlStep([config.adminEmail]),
239
+ ...(ports.kbStartupSteps ?? []),
240
+ ];
241
+ const kbStartupRunner = new KbStartupRunner({
242
+ // Getters, not values: the remote and the branch model can arrive from the
243
+ // setup screen after this object exists, and the setup-completion run is
244
+ // the first thing that needs them.
245
+ kbRepoUrl: () => settings.resolve('kbRepoUrl'),
246
+ gitUsername: () => settings.resolve('gitUsername') || 'x-access-token',
247
+ workspacesRoot: config.workspacesRoot,
248
+ kbDirName,
249
+ templateDir: config.kbTemplateDir,
250
+ defaultBranch: () => DEFAULT_BRANCH,
251
+ protectedBranches: () => [...PROTECTED_BRANCHES],
225
252
  // The deployment owner is the initial Admin of a freshly seeded KB — the
226
253
  // same answer `SEED_ADMIN_EMAILS` used to ask for a second time.
227
- [config.adminEmail],
228
- () => settings.resolve('gitUsername') || 'x-access-token',
229
- // Root folders this distribution reserves on top of core's two. A plain
230
- // value, not a getter: these are named in the composition root rather than
231
- // collected on the setup screen, so there is nothing to re-read.
232
- ports.kbExtraRootDirs ?? [],
233
- );
234
- workspaceService.setSeedService(kbSeedService);
254
+ seedAdminEmails: [config.adminEmail],
255
+ steps: kbStartupSteps,
256
+ buildSeedTree: buildSeedTree(config.kbTemplateDir, extraDirs, [config.adminEmail]),
257
+ });
235
258
  // Shared, workspace-independent store for oversized `call_tool_chain` results,
236
259
  // read back via `read_file`. Sibling of `workspacesRoot`, never committed.
237
260
  const spillStore = new SpillStore(config.spillRoot);
@@ -260,15 +283,15 @@ export async function createCoreServices(
260
283
  const routineWritePolicy = new RoutineWritePolicyService();
261
284
  // Skills: discovered from the default-branch workspace only (global catalog).
262
285
  const skillService = new SkillService(workspaceService, accessControl, kbDirName);
263
- // Tool manuals: user-authored `*.tool` files under `Groups/` in the default
286
+ // Tool manuals: user-authored `*.tool` files under `Plugins/` in the default
264
287
  // branch — access-controlled like Skills, served to external agents via
265
288
  // `GET /api/agent/all-tools` and registered on the MCP proxy's UTCP client.
266
289
  const toolManualService = new ToolManualService(workspaceService, accessControl, kbDirName);
267
- // Groups: the folders under `Groups/` that carry a
290
+ // Plugins: the folders under `Plugins/` that carry a
268
291
  // team's skills AND the tools they need. Enumerated for EVERY authenticated
269
- // caller — a group they cannot read still exists for them, as a locked one —
292
+ // caller — a plugin they cannot read still exists for them, as a locked one —
270
293
  // with the counts read off the two catalogs above rather than a second scan.
271
- const groupIndexService = new GroupIndexService(
294
+ const pluginIndexService = new PluginIndexService(
272
295
  workspaceService,
273
296
  accessControl,
274
297
  skillService,
@@ -384,16 +407,28 @@ export async function createCoreServices(
384
407
  workflowHooks,
385
408
  );
386
409
 
387
- // Join requests: derived entirely from two copies of a group's `access.md`
410
+ // Join requests: derived entirely from two copies of a plugin's `access.md`
388
411
  // (the request's branch vs the default branch), so it holds no state — it
389
412
  // only needs to read files at refs and to close a request whose proposals
390
413
  // have all landed.
391
414
  const joinRequestsService = new JoinRequestsService(workspaceService, workflowService);
392
- // Group provisioning — the one privileged door that brings `Groups/<name>/`
393
- // folders into existence (named groups and personal folders alike). Commits
415
+ // Server-scoped MCP editing — the tool page's edit form. One server's truth
416
+ // spans a plugin's mcp.json AND plugin.json extensions block, and this is
417
+ // the ONE writer that rewrites both entries and commits them together (see
418
+ // McpServerEditService).
419
+ const mcpServerEditService = new McpServerEditService(
420
+ workspaceService,
421
+ workflowService,
422
+ accessControl,
423
+ toolManualService,
424
+ kbDirName,
425
+ );
426
+
427
+ // Plugin provisioning — the one privileged door that brings `Plugins/<name>/`
428
+ // folders into existence (named plugins and personal folders alike). Commits
394
429
  // INLINE through the same pipeline the pending-commits worker uses, so the
395
430
  // folder's rules are at HEAD before the endpoint answers.
396
- const groupProvisionService = new GroupProvisionService(
431
+ const pluginProvisionService = new PluginProvisionService(
397
432
  workspaceService,
398
433
  workflowService,
399
434
  accessControl,
@@ -422,16 +457,16 @@ export async function createCoreServices(
422
457
  // caches are independent, so the split preserves behavior.)
423
458
  fileChangeNotifier.onFilesChanged(({ branch, paths }) => {
424
459
  if (branch !== DEFAULT_BRANCH) return;
425
- // Skills, tools and the group index all live under `Groups/`, so one
460
+ // Skills, tools and the plugin index all live under `Plugins/`, so one
426
461
  // touch check drives all three caches. An access grant lands as a
427
- // default-branch change to `Groups/<group>/access.md`, so this is also
428
- // what makes a newly-granted group unlock within one round-trip instead
462
+ // default-branch change to `Plugins/<plugin>/access.md`, so this is also
463
+ // what makes a newly-granted plugin unlock within one round-trip instead
429
464
  // of one TTL.
430
- const touched = paths.some((p) => p.startsWith(`${kbDirName}/${GROUPS_DIR}/`));
465
+ const touched = paths.some((p) => p.startsWith(`${kbDirName}/${PLUGINS_DIR}/`));
431
466
  if (touched) {
432
467
  toolManualService.invalidate();
433
468
  skillService.invalidate();
434
- groupIndexService.invalidate();
469
+ pluginIndexService.invalidate();
435
470
  }
436
471
  });
437
472
 
@@ -448,7 +483,7 @@ export async function createCoreServices(
448
483
  if (!('branch' in event) || event.branch !== DEFAULT_BRANCH) return;
449
484
  toolManualService.invalidate();
450
485
  skillService.invalidate();
451
- groupIndexService.invalidate();
486
+ pluginIndexService.invalidate();
452
487
  });
453
488
 
454
489
  // Admin = `Admin` role in roles.yaml, resolved through the access model on the
@@ -567,7 +602,12 @@ export async function createCoreServices(
567
602
  });
568
603
  // RFC 9728 pointer carried on every MCP 401 challenge so OAuth-capable
569
604
  // clients discover the AS. Single source of truth for the resource id.
605
+ // Userinfo is stripped for the same reason the server's own copy strips it
606
+ // (see create-core-server.ts): the pointer rides an unauthenticated 401
607
+ // challenge, and a `user:pass@` from PUBLIC_BACKEND_URL must not ride along.
570
608
  const mcpResourceUrl = new URL('/api/mcp', config.publicBackendUrl);
609
+ mcpResourceUrl.username = '';
610
+ mcpResourceUrl.password = '';
571
611
  const mcpResourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(mcpResourceUrl);
572
612
  const mcpAuthMiddleware = createMcpAuthMiddleware(
573
613
  authService,
@@ -675,8 +715,9 @@ export async function createCoreServices(
675
715
  return {
676
716
  config,
677
717
  db,
718
+ mcpServerEditService,
678
719
  workspaceService,
679
- kbSeedService,
720
+ kbStartupRunner,
680
721
  settings,
681
722
  kbDirName,
682
723
  spillStore,
@@ -687,8 +728,8 @@ export async function createCoreServices(
687
728
  skillService,
688
729
  pendingSkillsService,
689
730
  toolManualService,
690
- groupIndexService,
691
- groupProvisionService,
731
+ pluginIndexService,
732
+ pluginProvisionService,
692
733
  joinRequestsService,
693
734
  authService,
694
735
  authMiddleware,
package/src/index.ts CHANGED
@@ -1,69 +1,69 @@
1
- /**
2
- * @bevel-software/platform-core-backend — the open-source core of the Bevel platform:
3
- * git-backed knowledge workspace, workflow (branches / change requests /
4
- * locks / SSE), skills, tool manuals + registry + auth, secrets vault, access
5
- * control, diff/backup ledger, code-mode and the remote MCP surface.
6
- *
7
- * Composition entry points:
8
- * - `new CoreConfig()` — env-driven core configuration
9
- * - `createCoreServices(config, ports)` — construct every core service
10
- * - `createCoreServer(core, ext, opts)` — the Express app (fixed mount order)
11
- *
12
- * Importing this package ALSO applies the global Express `Request`
13
- * augmentation (`req.userId` / `req.userEmail` / token payloads) — the
14
- * middleware modules re-exported below carry the `declare global` blocks, so
15
- * consumers get the augmented types without a side-effect import of their own.
16
- *
17
- * Finer-grained internals are available via the `./modules/*` subpath exports.
18
- */
19
-
20
- export { CoreConfig } from './core-config.js';
21
- export { createCoreServices, type CoreServices } from './core/create-core-services.js';
22
- export {
23
- createCoreServer,
24
- type ServerExtensions,
25
- type ToolSurfaceCtx,
26
- } from './core/create-core-server.js';
27
- export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
28
-
29
- // Packaged assets (migrations/, kb-template/) + the migration runners.
30
- export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
31
- export {
32
- runCoreMigrations,
33
- runEnterpriseMigrations,
34
- } from './modules/database/migrate.js';
35
- export { getDb, type Database } from './modules/database/connection.js';
36
-
37
- // Build identity surfaced by GET /api/health.
38
- export { GIT_SHA, resolveGitSha } from './version.js';
39
-
40
- // Express `Request` augmentation carriers (userId/userEmail — auth.middleware;
41
- // token payloads — tool-auth + external-api-key interface). Re-exported by
42
- // VALUE/type so the `declare global` blocks land in this package's public
43
- // declaration graph.
44
- export { createAuthMiddleware, AUTH_COOKIE_NAME } from './modules/auth/auth.middleware.js';
45
- export {
46
- createToolAuthMiddleware,
47
- createManualAuthMiddleware,
48
- } from './modules/tool-auth/tool-auth.middleware.js';
49
- export type {
50
- IExternalApiKeyService,
51
- ExternalApiKeySummary,
52
- } from './modules/tool-auth/external-api-key.interface.js';
53
-
54
- // Key port/seam types an overlay implements.
55
- export type { ISessionSink } from './modules/workspace/session-sink.js';
56
- export type {
57
- ISystemNoticeSink,
58
- RecoveryAgentRunner,
59
- } from './modules/workflow/pending-commits.worker.js';
60
- export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
61
- export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
62
- export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
63
- export {
64
- WorkflowHooks,
65
- type CommitValidationHook,
66
- type CommitValidationContext,
67
- type PreWriteHook,
68
- type PreWriteContext,
69
- } from './modules/workflow/workflow-hooks.js';
1
+ /**
2
+ * @bevel-software/platform-core-backend — the open-source core of the Bevel platform:
3
+ * git-backed knowledge workspace, workflow (branches / change requests /
4
+ * locks / SSE), skills, tool manuals + registry + auth, secrets vault, access
5
+ * control, diff/backup ledger, code-mode and the remote MCP surface.
6
+ *
7
+ * Composition entry points:
8
+ * - `new CoreConfig()` — env-driven core configuration
9
+ * - `createCoreServices(config, ports)` — construct every core service
10
+ * - `createCoreServer(core, ext, opts)` — the Express app (fixed mount order)
11
+ *
12
+ * Importing this package ALSO applies the global Express `Request`
13
+ * augmentation (`req.userId` / `req.userEmail` / token payloads) — the
14
+ * middleware modules re-exported below carry the `declare global` blocks, so
15
+ * consumers get the augmented types without a side-effect import of their own.
16
+ *
17
+ * Finer-grained internals are available via the `./modules/*` subpath exports.
18
+ */
19
+
20
+ export { CoreConfig } from './core-config.js';
21
+ export { createCoreServices, type CoreServices } from './core/create-core-services.js';
22
+ export {
23
+ createCoreServer,
24
+ type ServerExtensions,
25
+ type ToolSurfaceCtx,
26
+ } from './core/create-core-server.js';
27
+ export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
28
+
29
+ // Packaged assets (migrations/, kb-template/) + the migration runners.
30
+ export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
31
+ export {
32
+ runCoreMigrations,
33
+ runEnterpriseMigrations,
34
+ } from './modules/database/migrate.js';
35
+ export { getDb, type Database } from './modules/database/connection.js';
36
+
37
+ // Build identity surfaced by GET /api/health.
38
+ export { GIT_SHA, resolveGitSha } from './version.js';
39
+
40
+ // Express `Request` augmentation carriers (userId/userEmail — auth.middleware;
41
+ // token payloads — tool-auth + external-api-key interface). Re-exported by
42
+ // VALUE/type so the `declare global` blocks land in this package's public
43
+ // declaration graph.
44
+ export { createAuthMiddleware, AUTH_COOKIE_NAME } from './modules/auth/auth.middleware.js';
45
+ export {
46
+ createToolAuthMiddleware,
47
+ createManualAuthMiddleware,
48
+ } from './modules/tool-auth/tool-auth.middleware.js';
49
+ export type {
50
+ IExternalApiKeyService,
51
+ ExternalApiKeySummary,
52
+ } from './modules/tool-auth/external-api-key.interface.js';
53
+
54
+ // Key port/seam types an overlay implements.
55
+ export type { ISessionSink } from './modules/workspace/session-sink.js';
56
+ export type {
57
+ ISystemNoticeSink,
58
+ RecoveryAgentRunner,
59
+ } from './modules/workflow/pending-commits.worker.js';
60
+ export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
61
+ export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
62
+ export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
63
+ export {
64
+ WorkflowHooks,
65
+ type CommitValidationHook,
66
+ type CommitValidationContext,
67
+ type PreWriteHook,
68
+ type PreWriteContext,
69
+ } from './modules/workflow/workflow-hooks.js';
@@ -1,98 +1,98 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import fs from 'node:fs/promises';
5
- import path from 'node:path';
6
- import os from 'node:os';
7
-
8
- import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
- import { AccessControlService } from '../access-control.service.js';
10
-
11
- const execFileAsync = promisify(execFile);
12
- const PROCESS_MAP_DIR = 'knowledge-base';
13
-
14
- /**
15
- * `canWriteBatchAtRef` / the batched at-ref own-entries read — backed by ONE
16
- * `git cat-file --batch` process whose length-prefixed output we parse by
17
- * hand, so exercise it against a REAL git repo: found blobs, a path missing
18
- * at the ref (the `<spec> missing` line), a path with spaces, and frontmatter
19
- * deny resolution — all in one batch, order-aligned.
20
- */
21
- describe('AccessControlService — at-ref batch reads (git cat-file --batch)', () => {
22
- let root: string;
23
- let repo: string;
24
- let svc: AccessControlService;
25
- const workspaceId = 'ws-atref-1';
26
- const admin = 'razvan@bevel.software';
27
-
28
- async function git(...args: string[]): Promise<void> {
29
- await execFileAsync('git', ['-C', repo, ...args]);
30
- }
31
-
32
- beforeAll(async () => {
33
- root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-atref-'));
34
- const workspaceDir = path.join(root, workspaceId);
35
- repo = path.join(workspaceDir, PROCESS_MAP_DIR);
36
- await fs.mkdir(path.join(repo, 'Knowledge'), { recursive: true });
37
-
38
- await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n');
39
- await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
40
- await fs.writeFile(path.join(repo, 'Knowledge/plain.md'), '# plain\n');
41
- await fs.writeFile(
42
- path.join(repo, 'Knowledge/denied.md'),
43
- '---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
44
- );
45
- await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
46
-
47
- await execFileAsync('git', ['init', '-b', 'main', repo]);
48
- await git('config', 'user.email', 'test@example.com');
49
- await git('config', 'user.name', 'Test');
50
- await git('add', '-A');
51
- await git('commit', '-m', 'seed');
52
-
53
- const stub = {
54
- getWorkspacePath: async () => workspaceDir,
55
- // Fixture repo has no remote; at-ref model loading fetches best-effort.
56
- ensureRemotesFetched: async () => undefined,
57
- } as unknown as WorkspaceService;
58
- svc = new AccessControlService(stub, PROCESS_MAP_DIR);
59
- });
60
-
61
- afterAll(async () => {
62
- await fs.rm(root, { recursive: true, force: true });
63
- });
64
-
65
- it('resolves a mixed batch in one pass: grants, frontmatter deny, spaces, and a missing path', async () => {
66
- const paths = [
67
- 'Knowledge/plain.md', // dir-chain Admin grant → true
68
- 'Knowledge/denied.md', // own frontmatter deny → false
69
- 'Knowledge/with space.md', // found blob whose SPEC contains spaces → true
70
- 'Knowledge/not-there.md', // missing at the ref (`<spec> missing` line) → dir-chain verdict
71
- ];
72
- const map = await svc.canWriteBatchAtRef(workspaceId, 'main', admin, paths);
73
- expect(map).not.toBeNull();
74
- expect(map!.get('Knowledge/plain.md')).toBe(true);
75
- expect(map!.get('Knowledge/denied.md')).toBe(false);
76
- expect(map!.get('Knowledge/with space.md')).toBe(true);
77
- expect(map!.get('Knowledge/not-there.md')).toBe(true);
78
- });
79
-
80
- it('matches the single-path at-ref resolution exactly', async () => {
81
- // The batch is a pure performance change — verdicts must be identical to
82
- // the per-path `git show` route the single-path helper still uses.
83
- expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/denied.md')).toBe(false);
84
- expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/plain.md')).toBe(true);
85
- });
86
-
87
- it('eligibleWritersForPathsAtRef honours the same batched own-entries', async () => {
88
- const map = await svc.eligibleWritersForPathsAtRef(workspaceId, 'main', [
89
- 'Knowledge/plain.md',
90
- 'Knowledge/denied.md',
91
- ]);
92
- expect(map).not.toBeNull();
93
- expect(map!.get('Knowledge/plain.md')!.roles).toContain('Admin');
94
- // The per-user deny strips razvan from the expanded email set.
95
- expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
96
- expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
97
- });
98
- });
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+
8
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
+ import { AccessControlService } from '../access-control.service.js';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const PROCESS_MAP_DIR = 'knowledge-base';
13
+
14
+ /**
15
+ * `canWriteBatchAtRef` / the batched at-ref own-entries read — backed by ONE
16
+ * `git cat-file --batch` process whose length-prefixed output we parse by
17
+ * hand, so exercise it against a REAL git repo: found blobs, a path missing
18
+ * at the ref (the `<spec> missing` line), a path with spaces, and frontmatter
19
+ * deny resolution — all in one batch, order-aligned.
20
+ */
21
+ describe('AccessControlService — at-ref batch reads (git cat-file --batch)', () => {
22
+ let root: string;
23
+ let repo: string;
24
+ let svc: AccessControlService;
25
+ const workspaceId = 'ws-atref-1';
26
+ const admin = 'razvan@bevel.software';
27
+
28
+ async function git(...args: string[]): Promise<void> {
29
+ await execFileAsync('git', ['-C', repo, ...args]);
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-atref-'));
34
+ const workspaceDir = path.join(root, workspaceId);
35
+ repo = path.join(workspaceDir, PROCESS_MAP_DIR);
36
+ await fs.mkdir(path.join(repo, 'Knowledge'), { recursive: true });
37
+
38
+ await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n');
39
+ await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
40
+ await fs.writeFile(path.join(repo, 'Knowledge/plain.md'), '# plain\n');
41
+ await fs.writeFile(
42
+ path.join(repo, 'Knowledge/denied.md'),
43
+ '---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
44
+ );
45
+ await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
46
+
47
+ await execFileAsync('git', ['init', '-b', 'main', repo]);
48
+ await git('config', 'user.email', 'test@example.com');
49
+ await git('config', 'user.name', 'Test');
50
+ await git('add', '-A');
51
+ await git('commit', '-m', 'seed');
52
+
53
+ const stub = {
54
+ getWorkspacePath: async () => workspaceDir,
55
+ // Fixture repo has no remote; at-ref model loading fetches best-effort.
56
+ ensureRemotesFetched: async () => undefined,
57
+ } as unknown as WorkspaceService;
58
+ svc = new AccessControlService(stub, PROCESS_MAP_DIR);
59
+ });
60
+
61
+ afterAll(async () => {
62
+ await fs.rm(root, { recursive: true, force: true });
63
+ });
64
+
65
+ it('resolves a mixed batch in one pass: grants, frontmatter deny, spaces, and a missing path', async () => {
66
+ const paths = [
67
+ 'Knowledge/plain.md', // dir-chain Admin grant → true
68
+ 'Knowledge/denied.md', // own frontmatter deny → false
69
+ 'Knowledge/with space.md', // found blob whose SPEC contains spaces → true
70
+ 'Knowledge/not-there.md', // missing at the ref (`<spec> missing` line) → dir-chain verdict
71
+ ];
72
+ const map = await svc.canWriteBatchAtRef(workspaceId, 'main', admin, paths);
73
+ expect(map).not.toBeNull();
74
+ expect(map!.get('Knowledge/plain.md')).toBe(true);
75
+ expect(map!.get('Knowledge/denied.md')).toBe(false);
76
+ expect(map!.get('Knowledge/with space.md')).toBe(true);
77
+ expect(map!.get('Knowledge/not-there.md')).toBe(true);
78
+ });
79
+
80
+ it('matches the single-path at-ref resolution exactly', async () => {
81
+ // The batch is a pure performance change — verdicts must be identical to
82
+ // the per-path `git show` route the single-path helper still uses.
83
+ expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/denied.md')).toBe(false);
84
+ expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/plain.md')).toBe(true);
85
+ });
86
+
87
+ it('eligibleWritersForPathsAtRef honours the same batched own-entries', async () => {
88
+ const map = await svc.eligibleWritersForPathsAtRef(workspaceId, 'main', [
89
+ 'Knowledge/plain.md',
90
+ 'Knowledge/denied.md',
91
+ ]);
92
+ expect(map).not.toBeNull();
93
+ expect(map!.get('Knowledge/plain.md')!.roles).toContain('Admin');
94
+ // The per-user deny strips razvan from the expanded email set.
95
+ expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
96
+ expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
97
+ });
98
+ });