@bevel-software/platform-core-backend 0.29.0 → 0.31.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 (307) hide show
  1. package/agent-guide/access-control.md +49 -4
  2. package/agent-guide/html-views.md +76 -2
  3. package/dist/__tests__/test-repositories.d.ts +3 -0
  4. package/dist/__tests__/test-repositories.d.ts.map +1 -0
  5. package/dist/__tests__/test-repositories.js +18 -0
  6. package/dist/__tests__/test-repositories.js.map +1 -0
  7. package/dist/core/catalog-revision.js +3 -3
  8. package/dist/core/create-core-server.d.ts.map +1 -1
  9. package/dist/core/create-core-server.js +3 -2
  10. package/dist/core/create-core-server.js.map +1 -1
  11. package/dist/core/create-core-services.d.ts +3 -0
  12. package/dist/core/create-core-services.d.ts.map +1 -1
  13. package/dist/core/create-core-services.js +35 -5
  14. package/dist/core/create-core-services.js.map +1 -1
  15. package/dist/core/lifecycle.d.ts +8 -0
  16. package/dist/core/lifecycle.d.ts.map +1 -1
  17. package/dist/core/lifecycle.js +4 -0
  18. package/dist/core/lifecycle.js.map +1 -1
  19. package/dist/modules/access/access-control.interface.d.ts +20 -0
  20. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  21. package/dist/modules/access/access-control.service.d.ts +32 -1
  22. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-control.service.js +135 -17
  24. package/dist/modules/access/access-control.service.js.map +1 -1
  25. package/dist/modules/access/access.routes.d.ts.map +1 -1
  26. package/dist/modules/access/access.routes.js +20 -6
  27. package/dist/modules/access/access.routes.js.map +1 -1
  28. package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
  29. package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
  30. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  31. package/dist/modules/audit/agent-audit.service.js +66 -6
  32. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  33. package/dist/modules/audit/audit.contract.d.ts +52 -8
  34. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  35. package/dist/modules/audit/audit.contract.js +37 -1
  36. package/dist/modules/audit/audit.contract.js.map +1 -1
  37. package/dist/modules/audit/audit.routes.d.ts +1 -1
  38. package/dist/modules/audit/audit.routes.d.ts.map +1 -1
  39. package/dist/modules/audit/audit.routes.js +15 -13
  40. package/dist/modules/audit/audit.routes.js.map +1 -1
  41. package/dist/modules/audit/request-audit.d.ts +21 -10
  42. package/dist/modules/audit/request-audit.d.ts.map +1 -1
  43. package/dist/modules/audit/request-audit.js +26 -10
  44. package/dist/modules/audit/request-audit.js.map +1 -1
  45. package/dist/modules/database/core-schema.d.ts +22 -3
  46. package/dist/modules/database/core-schema.d.ts.map +1 -1
  47. package/dist/modules/database/core-schema.js +15 -6
  48. package/dist/modules/database/core-schema.js.map +1 -1
  49. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  50. package/dist/modules/diff/diff.service.js +7 -1
  51. package/dist/modules/diff/diff.service.js.map +1 -1
  52. package/dist/modules/kb-fs/git-dir.d.ts +21 -0
  53. package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
  54. package/dist/modules/kb-fs/git-dir.js +58 -0
  55. package/dist/modules/kb-fs/git-dir.js.map +1 -0
  56. package/dist/modules/kb-fs/index.d.ts +3 -0
  57. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  58. package/dist/modules/kb-fs/index.js +3 -1
  59. package/dist/modules/kb-fs/index.js.map +1 -1
  60. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/locking-filesystem.js +3 -12
  62. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  63. package/dist/modules/kb-fs/mutex.d.ts +11 -0
  64. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  65. package/dist/modules/kb-fs/mutex.js +14 -0
  66. package/dist/modules/kb-fs/mutex.js.map +1 -1
  67. package/dist/modules/kb-fs/ref-files.d.ts +63 -0
  68. package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
  69. package/dist/modules/kb-fs/ref-files.js +249 -0
  70. package/dist/modules/kb-fs/ref-files.js.map +1 -0
  71. package/dist/modules/mcp/mcp-auth.middleware.d.ts +14 -0
  72. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  73. package/dist/modules/mcp/mcp-auth.middleware.js +4 -0
  74. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  75. package/dist/modules/mcp/mcp.routes.d.ts +5 -0
  76. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  77. package/dist/modules/mcp/mcp.routes.js +67 -0
  78. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  79. package/dist/modules/mcp/mcp.service.d.ts +107 -27
  80. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  81. package/dist/modules/mcp/mcp.service.js +257 -50
  82. package/dist/modules/mcp/mcp.service.js.map +1 -1
  83. package/dist/modules/mcp/request-context.d.ts +25 -0
  84. package/dist/modules/mcp/request-context.d.ts.map +1 -0
  85. package/dist/modules/mcp/request-context.js +11 -0
  86. package/dist/modules/mcp/request-context.js.map +1 -0
  87. package/dist/modules/mcp/shared-definitions.d.ts +56 -0
  88. package/dist/modules/mcp/shared-definitions.d.ts.map +1 -0
  89. package/dist/modules/mcp/shared-definitions.js +117 -0
  90. package/dist/modules/mcp/shared-definitions.js.map +1 -0
  91. package/dist/modules/mcp/surface-cache.d.ts +70 -0
  92. package/dist/modules/mcp/surface-cache.d.ts.map +1 -0
  93. package/dist/modules/mcp/surface-cache.js +104 -0
  94. package/dist/modules/mcp/surface-cache.js.map +1 -0
  95. package/dist/modules/plugins/teams.routes.d.ts +4 -0
  96. package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
  97. package/dist/modules/plugins/teams.routes.js +9 -1
  98. package/dist/modules/plugins/teams.routes.js.map +1 -1
  99. package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
  100. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
  101. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  102. package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
  103. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  104. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  105. package/dist/modules/tool-helpers/tool-context.js +33 -12
  106. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  107. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
  108. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -2
  109. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  110. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  111. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  112. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +3 -14
  113. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  114. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +18 -1
  115. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  116. package/dist/modules/workflow/agent-tools/workflow.tools.js +453 -2
  117. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  118. package/dist/modules/workflow/git/git.service.d.ts +142 -4
  119. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  120. package/dist/modules/workflow/git/git.service.js +645 -128
  121. package/dist/modules/workflow/git/git.service.js.map +1 -1
  122. package/dist/modules/workflow/read-on-branch.d.ts +35 -0
  123. package/dist/modules/workflow/read-on-branch.d.ts.map +1 -0
  124. package/dist/modules/workflow/read-on-branch.js +58 -0
  125. package/dist/modules/workflow/read-on-branch.js.map +1 -0
  126. package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
  127. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  128. package/dist/modules/workflow/workflow.routes.js +22 -41
  129. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  130. package/dist/modules/workflow/workflow.service.d.ts +48 -8
  131. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  132. package/dist/modules/workflow/workflow.service.js +163 -16
  133. package/dist/modules/workflow/workflow.service.js.map +1 -1
  134. package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
  135. package/dist/modules/workspace/agent-access.gate.js +2 -2
  136. package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
  137. package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
  138. package/dist/modules/workspace/bounded-matcher.js +261 -0
  139. package/dist/modules/workspace/bounded-matcher.js.map +1 -0
  140. package/dist/modules/workspace/fetch-instant.d.ts +9 -0
  141. package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
  142. package/dist/modules/workspace/fetch-instant.js +13 -0
  143. package/dist/modules/workspace/fetch-instant.js.map +1 -0
  144. package/dist/modules/workspace/index.d.ts +9 -0
  145. package/dist/modules/workspace/index.d.ts.map +1 -1
  146. package/dist/modules/workspace/index.js +10 -0
  147. package/dist/modules/workspace/index.js.map +1 -1
  148. package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
  149. package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
  150. package/dist/modules/workspace/kb-repository.contract.js +19 -0
  151. package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
  152. package/dist/modules/workspace/kb-repository.d.ts +236 -0
  153. package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
  154. package/dist/modules/workspace/kb-repository.js +811 -0
  155. package/dist/modules/workspace/kb-repository.js.map +1 -0
  156. package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
  157. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
  158. package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
  159. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
  160. package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
  161. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  162. package/dist/modules/workspace/routine-write-policy.js +0 -5
  163. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  164. package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
  165. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
  166. package/dist/modules/workspace/set-aside-clone.js +139 -0
  167. package/dist/modules/workspace/set-aside-clone.js.map +1 -1
  168. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
  169. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  170. package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
  171. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  172. package/dist/modules/workspace/workspace.service.d.ts +68 -32
  173. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  174. package/dist/modules/workspace/workspace.service.js +240 -161
  175. package/dist/modules/workspace/workspace.service.js.map +1 -1
  176. package/dist/modules/workspace/workspace.tools.d.ts +13 -4
  177. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  178. package/dist/modules/workspace/workspace.tools.js +162 -336
  179. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  180. package/dist/shared/git.contract.d.ts +7 -0
  181. package/dist/shared/git.contract.d.ts.map +1 -1
  182. package/dist/shared/git.contract.js +9 -0
  183. package/dist/shared/git.contract.js.map +1 -1
  184. package/dist/shared/no-link-below.d.ts +10 -0
  185. package/dist/shared/no-link-below.d.ts.map +1 -0
  186. package/dist/shared/no-link-below.js +30 -0
  187. package/dist/shared/no-link-below.js.map +1 -0
  188. package/dist/test-setup.js +4 -0
  189. package/dist/test-setup.js.map +1 -1
  190. package/migrations/0018_agent_events_page.sql +11 -0
  191. package/migrations/meta/0018_snapshot.json +2429 -0
  192. package/migrations/meta/_journal.json +8 -1
  193. package/package.json +3 -3
  194. package/src/__tests__/test-repositories.ts +22 -0
  195. package/src/__tests__/workspace-service.ts +69 -0
  196. package/src/core/__tests__/lifecycle.test.ts +16 -0
  197. package/src/core/catalog-revision.ts +3 -3
  198. package/src/core/create-core-server.ts +3 -2
  199. package/src/core/create-core-services.ts +39 -4
  200. package/src/core/lifecycle.ts +10 -0
  201. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +144 -4
  202. package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
  203. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
  204. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  205. package/src/modules/access/access-control.interface.ts +22 -0
  206. package/src/modules/access/access-control.service.ts +138 -15
  207. package/src/modules/access/access.routes.ts +22 -6
  208. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +22 -1
  209. package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
  210. package/src/modules/audit/__tests__/agent-audit.service.test.ts +78 -1
  211. package/src/modules/audit/__tests__/audit.routes.test.ts +43 -5
  212. package/src/modules/audit/agent-audit.service.ts +66 -6
  213. package/src/modules/audit/audit.contract.ts +62 -10
  214. package/src/modules/audit/audit.routes.ts +14 -12
  215. package/src/modules/audit/request-audit.ts +28 -13
  216. package/src/modules/database/core-schema.ts +15 -6
  217. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
  218. package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
  219. package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
  220. package/src/modules/diff/diff.service.ts +6 -4
  221. package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
  222. package/src/modules/kb-fs/__tests__/mutex.canonical.test.ts +57 -0
  223. package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
  224. package/src/modules/kb-fs/git-dir.ts +53 -0
  225. package/src/modules/kb-fs/index.ts +3 -0
  226. package/src/modules/kb-fs/locking-filesystem.ts +3 -12
  227. package/src/modules/kb-fs/mutex.ts +16 -0
  228. package/src/modules/kb-fs/ref-files.ts +279 -0
  229. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +7 -1
  230. package/src/modules/mcp/__tests__/mcp.routes.page.test.ts +115 -0
  231. package/src/modules/mcp/__tests__/mcp.surface-cache.test.ts +521 -0
  232. package/src/modules/mcp/__tests__/shared-definitions.test.ts +28 -0
  233. package/src/modules/mcp/__tests__/surface-cache.test.ts +127 -0
  234. package/src/modules/mcp/mcp-auth.middleware.ts +21 -0
  235. package/src/modules/mcp/mcp.routes.ts +68 -0
  236. package/src/modules/mcp/mcp.service.ts +315 -51
  237. package/src/modules/mcp/request-context.ts +34 -0
  238. package/src/modules/mcp/shared-definitions.ts +121 -0
  239. package/src/modules/mcp/surface-cache.ts +154 -0
  240. package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
  241. package/src/modules/plugins/teams.routes.ts +14 -1
  242. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
  243. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
  244. package/src/modules/tool-auth/internal-token.service.ts +1 -1
  245. package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
  246. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
  247. package/src/modules/tool-helpers/tool-context.ts +37 -18
  248. package/src/modules/tool-helpers/tool-handler.ts +2 -2
  249. package/src/modules/tool-helpers/tool.contract.ts +10 -2
  250. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +1 -1
  251. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +7 -2
  252. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +23 -12
  253. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +250 -4
  254. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +464 -4
  255. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +4 -22
  256. package/src/modules/workflow/agent-tools/workflow.tools.ts +462 -1
  257. package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
  258. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +76 -11
  259. package/src/modules/workflow/git/__tests__/git.service.mergeIntoBranch.test.ts +721 -0
  260. package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
  261. package/src/modules/workflow/git/git.service.ts +719 -126
  262. package/src/modules/workflow/read-on-branch.ts +79 -0
  263. package/src/modules/workflow/workflow-hooks.ts +1 -1
  264. package/src/modules/workflow/workflow.routes.ts +21 -39
  265. package/src/modules/workflow/workflow.service.ts +203 -16
  266. package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
  267. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
  268. package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
  269. package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
  270. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
  271. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
  272. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
  273. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
  274. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
  275. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
  276. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
  277. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
  278. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
  279. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
  280. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
  281. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
  282. package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
  283. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
  284. package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
  285. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
  286. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
  287. package/src/modules/workspace/__tests__/workspace.tools.test.ts +153 -298
  288. package/src/modules/workspace/agent-access.gate.ts +2 -2
  289. package/src/modules/workspace/bounded-matcher.ts +328 -0
  290. package/src/modules/workspace/fetch-instant.ts +13 -0
  291. package/src/modules/workspace/index.ts +23 -0
  292. package/src/modules/workspace/kb-repository.contract.ts +124 -0
  293. package/src/modules/workspace/kb-repository.ts +844 -0
  294. package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
  295. package/src/modules/workspace/routine-write-policy.ts +0 -15
  296. package/src/modules/workspace/set-aside-clone.ts +140 -0
  297. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
  298. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
  299. package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
  300. package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
  301. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
  302. package/src/modules/workspace/workspace.service.ts +247 -169
  303. package/src/modules/workspace/workspace.tools.ts +179 -365
  304. package/src/shared/__tests__/no-link-below.test.ts +45 -0
  305. package/src/shared/git.contract.ts +10 -0
  306. package/src/shared/no-link-below.ts +29 -0
  307. package/src/test-setup.ts +5 -0
@@ -0,0 +1,79 @@
1
+ import type { IAccessControl } from '../access/access-control.interface.js';
2
+ import { WorkflowDomainError } from '../../shared/domain-errors.js';
3
+
4
+ /**
5
+ * Which of `paths` `userEmail` may read at `origin/<base>`, as a membership
6
+ * set — ONE rule for the change-request reads (their file lists, judged at
7
+ * the request's base) and for `branch_status` (the paths a request would
8
+ * carry). A null answer (the ref, or its `roles.yaml`, does not resolve)
9
+ * withholds everything, which is the same reading the app's fork-point
10
+ * route gives it.
11
+ */
12
+ export async function readablePathsAt(
13
+ accessControl: Pick<IAccessControl, 'canReadBatchAtRef'>,
14
+ workspaceId: string,
15
+ userEmail: string,
16
+ base: string,
17
+ paths: string[],
18
+ ): Promise<Set<string>> {
19
+ if (paths.length === 0) return new Set();
20
+ const verdicts = await accessControl.canReadBatchAtRef(workspaceId, `origin/${base}`, userEmail, [...new Set(paths)]);
21
+ if (!verdicts) return new Set();
22
+ return new Set([...verdicts].filter(([, allowed]) => allowed).map(([p]) => p));
23
+ }
24
+
25
+ /**
26
+ * Whether `userEmail` may read `repoRelativePath` at EVERY one of `commits`
27
+ * — ONE rule for content served from another branch, for the app's compare
28
+ * route and the agent's history tools alike: the commits the service read
29
+ * the content at, pinned by the service inside its reservation (a diff's two
30
+ * sides, a file at a branch's published tip, the two commits a merge is of),
31
+ * so the verdict is about the bytes actually served, never about a branch
32
+ * ref a fetch may have moved between the verdict and the read. A commit
33
+ * whose rules cannot be read (null) grants nothing. An access-model FAILURE
34
+ * is not a null: it propagates with its own status (an unreadable access
35
+ * tree is the documented 503 a client retries).
36
+ */
37
+ export async function readableAtCommits(
38
+ accessControl: Pick<IAccessControl, 'canReadAtRef'>,
39
+ workspaceId: string,
40
+ userEmail: string,
41
+ repoRelativePath: string,
42
+ commits: readonly string[],
43
+ ): Promise<boolean> {
44
+ for (const sha of commits) {
45
+ if ((await accessControl.canReadAtRef(workspaceId, sha, userEmail, repoRelativePath)) !== true) return false;
46
+ }
47
+ return true;
48
+ }
49
+
50
+ /** The refusal `readableAtCommits` answers false with, as every caller words it: the branch named, never the commit. */
51
+ export class ReadOnBranchDeniedError extends WorkflowDomainError {
52
+ constructor(displayPath: string, branch: string) {
53
+ super(`You don't have permission to read "${displayPath}" on "${branch}".`, 403, {
54
+ kind: 'read-denied',
55
+ path: displayPath,
56
+ branch,
57
+ });
58
+ this.name = 'ReadOnBranchDeniedError';
59
+ }
60
+ }
61
+
62
+ /**
63
+ * `readableAtCommits` at every commit `branch` resolves to (the one its
64
+ * content was served from, and any other its local head or published tip
65
+ * names), thrown when it answers false.
66
+ */
67
+ export async function assertReadableAtCommits(
68
+ accessControl: Pick<IAccessControl, 'canReadAtRef'>,
69
+ workspaceId: string,
70
+ userEmail: string,
71
+ repoRelativePath: string,
72
+ commits: readonly string[],
73
+ branch: string,
74
+ displayPath: string = repoRelativePath,
75
+ ): Promise<void> {
76
+ if (!(await readableAtCommits(accessControl, workspaceId, userEmail, repoRelativePath, commits))) {
77
+ throw new ReadOnBranchDeniedError(displayPath, branch);
78
+ }
79
+ }
@@ -65,7 +65,7 @@ export interface AgentOperationContext {
65
65
  sessionId?: string;
66
66
  /**
67
67
  * The workspace-relative path the operation targets. Absent for a
68
- * write-capable operation with no resolvable target (`execute_command`).
68
+ * write-capable operation with no resolvable target.
69
69
  */
70
70
  wsPath?: string;
71
71
  /** The branch (draft) whose workspace the operation acts on. */
@@ -38,6 +38,7 @@ import type { WorkspaceService } from '../workspace/workspace.service.js';
38
38
  import { branchForWorkspaceId } from '../../shared/workspace-id.js';
39
39
  import type { WorkflowEventBus } from './event-bus.js';
40
40
  import { WorkflowDomainError } from '../../shared/domain-errors.js';
41
+ import { assertReadableAtCommits } from './read-on-branch.js';
41
42
  import { assertValidRelativePath } from '../kb-fs/branch-name.js';
42
43
  import { domainErrorBody } from '../../shared/http-errors.js';
43
44
  import { canonicalFileIdentity } from '../../shared/canonical-file-identity.js';
@@ -413,46 +414,27 @@ export function createWorkflowRoutes(
413
414
  // The other history endpoints serve commits of the URL workspace's own
414
415
  // branch, so its working-tree verdict covers what they return. This one
415
416
  // serves content of TWO caller-named branches — the verdict must come
416
- // from the refs actually being served, or a caller could pick a
417
- // workspace whose tree grants what the compared branches' trees deny.
418
- //
419
- // The comparison prefers a LOCAL ref (and the working tree for the
420
- // checked-out branch) over `origin/<branch>`, so both candidate refs are
421
- // authorized: every ref that RESOLVES must grant the read, and a branch
422
- // none of whose refs resolve is denied — never serve a ref that cannot
423
- // be authorized. (The working-tree side is additionally covered by the
424
- // workspace `canRead` above.)
425
- {
426
- const user = await requireUser(req, res);
427
- if (!user) return;
428
- const repoRelative = toKbRelative(pathParam, kbDirName)!;
429
- for (const branch of [fromBranch, toBranch]) {
430
- const verdicts: (boolean | null)[] = [];
431
- for (const ref of [`origin/${branch}`, branch]) {
432
- try {
433
- verdicts.push(await accessControl.canReadAtRef(req.params.id, ref, user.email, repoRelative));
434
- } catch (err) {
435
- // An access-model FAILURE is not an unresolvable ref: mapped to
436
- // null it would let the other candidate's grant serve a ref
437
- // whose authorization errored. Fail closed — but keep the
438
- // error's own status: an unreadable access tree is the
439
- // documented 503 a client retries, not a permanent 403 denial.
440
- const { status, body } = toHttpError(err);
441
- res.status(status).json(body);
442
- return;
443
- }
444
- }
445
- const denied = verdicts.some((v) => v === false);
446
- const unresolvable = verdicts.every((v) => v === null);
447
- if (denied || unresolvable) {
448
- res.status(403).json({ error: `You don't have permission to read "${pathParam}" on "${branch}".` });
449
- return;
450
- }
451
- }
452
- }
417
+ // from the commits actually served (`readableAtCommits`, the one rule
418
+ // this route and the agent's history tools share): the service pins the
419
+ // commit each side is read at and asks this verdict inside its
420
+ // reservation, before anything is said about the path at either, with
421
+ // every commit the branch resolves to — its local head and its published
422
+ // tip, all of which must grant, as this route has always required of
423
+ // both refs — never at a branch ref a fetch may move between a verdict
424
+ // and a read. (The
425
+ // working-tree side is additionally covered by the workspace `canRead`
426
+ // above.) An access-model failure keeps its own status: an unreadable
427
+ // access tree is the documented 503 a client retries, not a permanent
428
+ // 403 denial.
429
+ const user = await requireUser(req, res);
430
+ if (!user) return;
431
+ const repoRelative = toKbRelative(pathParam, kbDirName)!;
453
432
  try {
454
- const diff = await workflow.compareFile(req.params.id, pathParam, fromBranch, toBranch);
455
- res.json({ diff });
433
+ const compared = await workflow.compareFile(req.params.id, pathParam, fromBranch, toBranch, async (sides) => {
434
+ await assertReadableAtCommits(accessControl, req.params.id, user.email, repoRelative, sides.from, fromBranch, pathParam);
435
+ await assertReadableAtCommits(accessControl, req.params.id, user.email, repoRelative, sides.to, toBranch, pathParam);
436
+ });
437
+ res.json({ diff: compared.diff });
456
438
  } catch (err) {
457
439
  const { status, body } = toHttpError(err);
458
440
  res.status(status).json(body);
@@ -58,9 +58,11 @@ import type {
58
58
  IWorkflowService,
59
59
  MergeBranchOutcome,
60
60
  MergeChangeRequestOutcome,
61
+ MergeResolution,
61
62
  OpenChangeRequestInput,
62
63
  PostChangeRequestCommentInput,
63
64
  RemoteSyncPullResult,
65
+ UpdateBranchOutcome,
64
66
  } from '@bevel-software/platform-shared';
65
67
  import {
66
68
  isFolderPlaceholder,
@@ -68,6 +70,7 @@ import {
68
70
  isPlatformRestoreShape,
69
71
  } from '@bevel-software/platform-shared';
70
72
  import type { KbContext } from '../../shared/kb-context.js';
73
+ import { readableAtCommits, ReadOnBranchDeniedError } from './read-on-branch.js';
71
74
  import { and, desc, eq, inArray, isNotNull, isNull, lt, ne, or, sql } from 'drizzle-orm';
72
75
  import type { Database } from '../database/connection.js';
73
76
  import { changeRequests } from '../database/schema.js';
@@ -421,22 +424,29 @@ export class WorkflowService implements IWorkflowService {
421
424
  * opposite directions (`A -> B` and `B -> A`), and nothing is ever held
422
425
  * while waiting for the other key.
423
426
  *
424
- * A PROTECTED target's key is NOT reserved. No path in the system can
425
- * delete a protected branch (`git.deleteBranch` throws
427
+ * A PROTECTED branch's key is NOT reserved, at either end. No path in the
428
+ * system can delete a protected branch (`git.deleteBranch` throws
426
429
  * `ProtectedBranchError` before any lock, `retireMergedSourceBranch`
427
430
  * returns early on one), so the key would exclude nothing. It would only
428
- * cost: nearly every request targets the protected default branch, so one
429
- * shared key there would serialize every open on the hottest path for the
430
- * duration of a fetch + merge + push + diff. The source needs no such
431
- * carve-out; `openChangeRequest` refuses protected sources before locking.
431
+ * cost: nearly every request targets the protected default branch, and
432
+ * every draft refreshes FROM it (`updateBranch`, the merge tool's sync),
433
+ * so one shared key there would serialize every open on the hottest path for the
434
+ * duration of a fetch + merge + push + diff. A protected SOURCE takes no
435
+ * key for the same reason: `openChangeRequest` refuses protected sources
436
+ * before locking, and a draft refreshing from the default branch is
437
+ * exactly a protected source.
432
438
  */
433
439
  private runOnBranchPair<T>(
434
440
  source: string,
435
441
  target: string,
436
442
  fn: () => Promise<T>,
437
443
  ): Promise<T> {
438
- const keys = [`branch:${source}`];
439
- if (!this.kb.isProtectedBranch(target)) keys.push(`branch:${target}`);
444
+ // A PROTECTED branch's key is not reserved at EITHER end (see above):
445
+ // nothing can delete one, so the key would exclude nothing and only
446
+ // serialize every draft that refreshes from the default branch behind
447
+ // every other. The same branch at both ends (a draft syncing its own
448
+ // shared state) takes its one key.
449
+ const keys = [...new Set([source, target].filter((b) => !this.kb.isProtectedBranch(b)).map((b) => `branch:${b}`))];
440
450
  return this.branchLifecycle.runAll(keys, fn);
441
451
  }
442
452
 
@@ -968,7 +978,17 @@ export class WorkflowService implements IWorkflowService {
968
978
  workspaceId: string,
969
979
  opts: { preserveMerges?: boolean } = {},
970
980
  ): Promise<boolean> {
971
- const { treeChanged } = await this.git.pull(workspaceId, opts);
981
+ return this.pullWorkspaceVia(() => this.git.pull(workspaceId, opts), workspaceId);
982
+ }
983
+
984
+ /**
985
+ * `pullWorkspace` over the pull it is given: the workspace's own
986
+ * (`git.pull`, which takes the workspace's reservation) or one a caller
987
+ * already holding that reservation hands in (a merge's publish). The
988
+ * announcement is the same either way.
989
+ */
990
+ private async pullWorkspaceVia(pull: () => Promise<{ treeChanged: boolean }>, workspaceId: string): Promise<boolean> {
991
+ const { treeChanged } = await pull();
972
992
  const defaultBranch = this.kb.defaultBranch;
973
993
  if (treeChanged && branchForWorkspaceId(workspaceId) === defaultBranch) {
974
994
  this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: defaultBranch });
@@ -1259,8 +1279,9 @@ export class WorkflowService implements IWorkflowService {
1259
1279
  path: string,
1260
1280
  fromBranch: string,
1261
1281
  toBranch: string,
1262
- ): Promise<string> {
1263
- return this.git.diffFileBetweenBranches(workspaceId, path, fromBranch, toBranch);
1282
+ authorize: (sides: { from: string[]; to: string[] }) => Promise<void>,
1283
+ ): Promise<{ diff: string; from: string; to: string }> {
1284
+ return this.git.diffFileBetweenBranches(workspaceId, path, fromBranch, toBranch, authorize);
1264
1285
  }
1265
1286
 
1266
1287
  fileAtForkPoint(
@@ -1893,8 +1914,33 @@ export class WorkflowService implements IWorkflowService {
1893
1914
  ): Promise<void> {
1894
1915
  // No explicit `undefined` third argument when there are no options: the
1895
1916
  // call shape stays the bare `push(workspaceId, user)` every caller made.
1896
- const push = () =>
1897
- opts ? this.git.push(workspaceId, user, opts) : this.git.push(workspaceId, user);
1917
+ return this.pushWithRecoveryVia(
1918
+ {
1919
+ push: () => (opts ? this.git.push(workspaceId, user, opts) : this.git.push(workspaceId, user)),
1920
+ pull: () => this.pullWorkspace(workspaceId, { preserveMerges: true }),
1921
+ },
1922
+ workspaceId,
1923
+ branch,
1924
+ targetPath,
1925
+ user,
1926
+ );
1927
+ }
1928
+
1929
+ /**
1930
+ * `pushWithRecovery`'s policy over the two moves it is made of — the push,
1931
+ * and the pull-rebase that keeps a stranded merge a merge — so that a
1932
+ * caller already holding the workspace's reservation (a merge publishing
1933
+ * itself, `updateBranch`) runs the SAME policy over its own unlocked moves,
1934
+ * and a push the host refuses answers one way there too.
1935
+ */
1936
+ private async pushWithRecoveryVia(
1937
+ ops: { push: () => Promise<void>; pull: () => Promise<unknown> },
1938
+ workspaceId: string,
1939
+ branch: string,
1940
+ targetPath: string,
1941
+ user: AuthUser,
1942
+ ): Promise<void> {
1943
+ const push = ops.push;
1898
1944
  try {
1899
1945
  await push();
1900
1946
  this.noteGitSyncOk(workspaceId, branch);
@@ -1915,7 +1961,7 @@ export class WorkflowService implements IWorkflowService {
1915
1961
  // with whatever push of the branch comes next — a save, a share —
1916
1962
  // and a plain rebase would flatten it into cherry-picks. With no
1917
1963
  // merge among the local commits the two spellings replay the same.
1918
- await this.pullWorkspace(workspaceId, { preserveMerges: true });
1964
+ await ops.pull();
1919
1965
  try {
1920
1966
  await push();
1921
1967
  } catch (retryErr) {
@@ -4182,8 +4228,8 @@ export class WorkflowService implements IWorkflowService {
4182
4228
  // while they are held: a bare read would let a person open the request
4183
4229
  // mid-merge and the agent would land its content anyway, which is the one
4184
4230
  // thing this tool exists to prevent. `runOnBranchPair` drops a protected
4185
- // target's key; the source key is held either way, and every competing
4186
- // request from this source takes it too.
4231
+ // branch's key at either end; a draft source's key is held, and every
4232
+ // competing request from this source takes it too.
4187
4233
  return this.runOnBranchPair(sourceBranch, targetBranch, async () => {
4188
4234
  // A person merges a change request. Only the request's OWN direction is
4189
4235
  // blocked: the target merged into the source is how a draft under review
@@ -4681,4 +4727,145 @@ export class WorkflowService implements IWorkflowService {
4681
4727
  return formatAffectedOwnersBlock(paths, resolved);
4682
4728
  }
4683
4729
 
4730
+ async updateBranch(
4731
+ workspaceId: string,
4732
+ user: AuthUser,
4733
+ opts: { from?: string } & (
4734
+ | { resolutions?: undefined; validateWrite?: undefined }
4735
+ | { resolutions: MergeResolution[]; validateWrite: (wsPath: string, content: string) => void | Promise<void> }
4736
+ ) = {},
4737
+ ): Promise<UpdateBranchOutcome> {
4738
+ const branch = branchForWorkspaceId(workspaceId);
4739
+ const from = opts.from ?? branch;
4740
+ // A resolution writes on the draft: it is somebody's to gate, and a
4741
+ // caller that brings none gates nothing (closed) — the type says so for
4742
+ // a compiled caller, this for any other.
4743
+ if ((opts.resolutions?.length ?? 0) > 0 && typeof opts.validateWrite !== 'function') {
4744
+ throw new Error('updateBranch: resolutions need a `validateWrite` to be asked before the merge');
4745
+ }
4746
+ if (this.kb.isProtectedBranch(branch)) {
4747
+ throw new WorkflowDomainError(
4748
+ `"${branch}" is protected: it is changed through change requests, never merged into directly.`,
4749
+ 403,
4750
+ { kind: 'protected-update-target', branch },
4751
+ );
4752
+ }
4753
+ // The one merge a person makes: a change request's. An update that would
4754
+ // land an OPEN request from `from` into this draft is that merge by
4755
+ // another name, and `mergeBranch`'s gate refuses it here too — asked once
4756
+ // before waiting, and again under the locks below, where the answer
4757
+ // counts. (The other direction, the request's target into its source, is
4758
+ // how a draft under review stays current, and is never blocked.)
4759
+ if (from !== branch) {
4760
+ const openBefore = await this.openRequestBlockingMerge(from, branch);
4761
+ if (openBefore !== null) throw new OpenChangeRequestBlocksMergeError(from, branch, openBefore);
4762
+ }
4763
+ // The merge takes what the REMOTE holds of `from`, and the push publishes
4764
+ // the branch: writes still being committed on either would be missed by
4765
+ // the first or swept up by the second. Waited for, as `mergeBranch` waits
4766
+ // — outside the lifecycle locks, which must not be held for seconds of
4767
+ // waiting on a queue.
4768
+ for (const b of from === branch ? [branch] : [from, branch]) {
4769
+ const settled = await this.awaitPendingCommits(b);
4770
+ if (!settled.ok) {
4771
+ const message = settled.needsAttention
4772
+ ? `A commit queued on "${b}" failed and needs a person: ${settled.needsAttention} ` +
4773
+ 'Nothing was merged. Tell the user; update again once it has been resolved.'
4774
+ : `${settled.pending} write(s) on "${b}" are still being committed. Nothing was merged. Retry shortly.`;
4775
+ return {
4776
+ kind: 'pending-commits',
4777
+ branch: b,
4778
+ pending: settled.pending,
4779
+ ...(settled.needsAttention ? { needsAttention: settled.needsAttention } : {}),
4780
+ message,
4781
+ };
4782
+ }
4783
+ }
4784
+ // Under the lifecycle lock of both branches, as `mergeBranch` and
4785
+ // `openChangeRequest` run: the "no open request" answer is only sound
4786
+ // while nobody can open one mid-merge, and a branch being merged into is
4787
+ // not one to delete or recreate meanwhile. A protected `from` takes no
4788
+ // key, and the branch's own shared state (from = branch) takes the one.
4789
+ return this.runOnBranchPair(from, branch, async () => {
4790
+ if (from !== branch) {
4791
+ const open = await this.openRequestBlockingMerge(from, branch);
4792
+ if (open !== null) throw new OpenChangeRequestBlocksMergeError(from, branch, open);
4793
+ }
4794
+ if (!(await this.git.remoteBranchExists(workspaceId, from))) {
4795
+ throw new WorkflowValidationError(`No branch named "${from}" on the shared repository.`);
4796
+ }
4797
+ const resolutions = opts.resolutions ?? [];
4798
+ const subject = from === branch ? `Merge the shared ${branch} into the local one` : `Merge ${from} into ${branch}`;
4799
+ const body =
4800
+ (from === branch
4801
+ ? `The branch had moved on the shared repository while this workspace held commits of its own; both are kept.`
4802
+ : `Brings the latest ${from} into ${branch}.`) +
4803
+ (resolutions.length > 0
4804
+ ? ` Conflicts resolved by ${user.name} <${user.email}> in: ${resolutions.map((r) => r.path).join(', ')}.`
4805
+ : '');
4806
+ // Published inside the merge's own reservation: nothing lands between
4807
+ // the merge commit and its push, and the push is EVERY workflow push's
4808
+ // `pushWithRecovery`, run over the merge's own unlocked moves — a
4809
+ // rejection replays the merge (kept as a merge) onto the moved branch
4810
+ // and pushes again; a replay that conflicts or a host that refuses
4811
+ // answers as every refused share answers, the banner noted and the
4812
+ // typed hand-off thrown, naming the branch (no single file is at issue)
4813
+ // and never a conflicted path. The merge commit stays local for the
4814
+ // next push of the branch to carry.
4815
+ return this.git.mergeIntoBranch(workspaceId, branch, from, user, {
4816
+ subject,
4817
+ body,
4818
+ resolutions,
4819
+ publish: (ops) =>
4820
+ this.pushWithRecoveryVia(
4821
+ { push: ops.push, pull: () => this.pullWorkspaceVia(ops.pullRebase, workspaceId) },
4822
+ workspaceId,
4823
+ branch,
4824
+ branch,
4825
+ user,
4826
+ ),
4827
+ // A resolution writes a file on the draft from what both branches
4828
+ // hold of it. It is authorized inside the merge's reservation, after
4829
+ // the fetch and before the merge touches the checkout — the draft's
4830
+ // tree clean at its own commit, so the rules read from it are the
4831
+ // draft's, never a rule file mid-merge: the caller must read the
4832
+ // file at the draft's commit and at the source commit the merge is
4833
+ // of, and may change it on the draft by the one rule every draft
4834
+ // write takes — the change gate `acquireLock` asks, not a write
4835
+ // permission a draft never requires.
4836
+ authorizeResolutions: async ({ ours, theirs }) => {
4837
+ for (const r of resolutions) {
4838
+ const rel = r.path.startsWith(`${this.kbDirName}/`) ? r.path.slice(this.kbDirName.length + 1) : r.path;
4839
+ // At the two COMMITS the merge is of — pinned by the merge itself,
4840
+ // never at a ref a concurrent fetch can move between this verdict
4841
+ // and the merge — by the one rule the tool shows a conflict by.
4842
+ if (!(await readableAtCommits(this.accessControl, workspaceId, user.email, rel, [ours, theirs]))) {
4843
+ throw new ReadOnBranchDeniedError(rel, from);
4844
+ }
4845
+ // The gate takes the WORKSPACE path, as `acquireLock` hands it
4846
+ // one: a repository-relative path reads as outside the knowledge
4847
+ // base there, which the gate waves through.
4848
+ await this.changeGate.assertMayChange(workspaceId, user.email, `${this.kbDirName}/${rel}`, 'file');
4849
+ // Content the resolution brings takes the caller's pre-disk write
4850
+ // gate, as its file writes do before a byte lands; a side kept
4851
+ // whole is a commit's own bytes and brings none.
4852
+ if (r.content !== undefined && opts.validateWrite) await opts.validateWrite(`${this.kbDirName}/${rel}`, r.content);
4853
+ }
4854
+ },
4855
+ });
4856
+ });
4857
+ }
4858
+
4859
+ fileAtBranch(
4860
+ workspaceId: string,
4861
+ path: string,
4862
+ branch: string,
4863
+ authorize: (commits: string[]) => Promise<void>,
4864
+ ): Promise<{ sha: string; ref: string; content: string | null }> {
4865
+ return this.git.readFileAtBranch(workspaceId, path, branch, authorize);
4866
+ }
4867
+
4868
+ changedPathsBetweenBranches(workspaceId: string, base: string, branch: string): Promise<string[]> {
4869
+ return this.git.changedPathsForPr(workspaceId, base, branch);
4870
+ }
4684
4871
  }
@@ -22,7 +22,6 @@ import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
22
22
  import { ToolDescriptionNotes } from '../agent-access.gate.js';
23
23
  import { SpillStore } from '../spill-store.js';
24
24
  import { DocExtractService } from '../file-readers/doc-extract.service.js';
25
- import { WorkspaceService } from '../workspace.service.js';
26
25
  import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
27
26
  import { AgentDownloadStore, DOWNLOAD_TOKEN_REFUSAL } from '../agent-download.store.js';
28
27
  import { attachmentDisposition, createAgentDownloadRoutes, createDownloadFetcherIdentifier } from '../agent-download.routes.js';
@@ -31,6 +30,7 @@ import jwt from 'jsonwebtoken';
31
30
  import { buildDownload, DOWNLOAD_PERMISSION_REQUIRED, NOT_FOUND } from '../agent-download.builder.js';
32
31
  import { READ_ONLY_CODE, type IWriteAccess } from '../../write-access/write-access.js';
33
32
  import { sharedFileRules } from '../../agent-instructions/shared-file-rules.js';
33
+ import { newWorkspaceService } from '../../../__tests__/workspace-service.js';
34
34
 
35
35
  /**
36
36
  * `request_file_download` end to end, over the REAL access resolver on a
@@ -136,7 +136,7 @@ async function start(options: StartOptions = {}): Promise<Started> {
136
136
  await mkdir(dirname(abs), { recursive: true });
137
137
  await writeFile(abs, contents);
138
138
  }
139
- const workspaceService = new WorkspaceService(
139
+ const workspaceService = newWorkspaceService(
140
140
  root,
141
141
  'https://example.invalid/kb.git',
142
142
  testKbContext({ kbDirName: KB }),