@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
@@ -3,9 +3,10 @@ import path from 'node:path';
3
3
  import { logger } from '../../shared/logging.js';
4
4
  const log = logger('access');
5
5
  import { isAbsence } from '../../shared/fs.contract.js';
6
- import { assertOneSpecPerLine, isGitTimeout } from '../../shared/git.contract.js';
6
+ import { assertOneSpecPerLine, isCommitSha, isGitTimeout } from '../../shared/git.contract.js';
7
7
  import { hashEmail } from '../../shared/email-identity.js';
8
8
  import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
9
+ import { readRevSha } from '../kb-fs/index.js';
9
10
  import { DEFAULT_KB_LAYOUT, PLUGIN_MANIFEST_FILE, isPersonalPluginDir, pluginIdentityOf, platformRestoreDestination, } from '@bevel-software/platform-shared';
10
11
  import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
11
12
  import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
@@ -1143,6 +1144,24 @@ export class AccessControlService {
1143
1144
  atRefNoRoles = new Set();
1144
1145
  atRefInFlight = new Map();
1145
1146
  static AT_REF_CACHE_MAX = 512;
1147
+ /**
1148
+ * A path's own access entries AT A COMMIT, keyed by commit + path. A blob
1149
+ * at a commit never changes, so neither does what it declares for itself:
1150
+ * no TTL, no invalidation, and no workspace in the key (every clone holds
1151
+ * the same objects). This is the read every at-ref gate makes after the
1152
+ * model: one `cat-file --batch` per call for the paths asked, which a
1153
+ * change-request listing made once per ref per listing for the same paths
1154
+ * at the same commits. Bounded FIFO; the bound only caps memory.
1155
+ */
1156
+ ownEntriesAtCommit = new Map();
1157
+ static OWN_ENTRIES_AT_COMMIT_MAX = 16_384;
1158
+ /** Paths a cached model keeps of its commit's tree, at most; a larger tree is not kept. */
1159
+ static TREE_INDEX_MAX_PATHS = 20_000;
1160
+ /** Paths held across every cached model's tree, at most; past it the oldest models let their tree go. */
1161
+ static TREE_INDEX_BUDGET = 500_000;
1162
+ treeIndexMaxPaths;
1163
+ treeIndexBudget;
1164
+ treePathsHeld = 0;
1146
1165
  /**
1147
1166
  * Canonicalised deployment-owner emails (see `AccessModel.deploymentOwners`).
1148
1167
  * Held on the service rather than read per-model because it comes from the
@@ -1171,13 +1190,22 @@ export class AccessControlService {
1171
1190
  * `deploymentOwners`: the fixtures that construct this service directly
1172
1191
  * run on the default layout.
1173
1192
  */
1174
- kb = { layout: DEFAULT_KB_LAYOUT }) {
1193
+ kb = { layout: DEFAULT_KB_LAYOUT },
1194
+ /**
1195
+ * How much of the commits' trees the at-ref models may hold (see
1196
+ * `TREE_INDEX_MAX_PATHS`, `TREE_INDEX_BUDGET`). The defaults are the
1197
+ * deployment's; a suite lowers them to reach the bounds with a small
1198
+ * repository.
1199
+ */
1200
+ limits = {}) {
1175
1201
  this.workspaceService = workspaceService;
1176
1202
  this.kbDirName = kbDirName;
1177
1203
  this.disk = disk;
1178
1204
  this.gitRunner = gitRunner;
1179
1205
  this.kb = kb;
1180
1206
  this.deploymentOwners = new Set(deploymentOwners.filter(Boolean).map((e) => canonicalEmail(e)));
1207
+ this.treeIndexMaxPaths = limits.treeIndexMaxPaths ?? AccessControlService.TREE_INDEX_MAX_PATHS;
1208
+ this.treeIndexBudget = limits.treeIndexBudget ?? AccessControlService.TREE_INDEX_BUDGET;
1181
1209
  }
1182
1210
  /**
1183
1211
  * Drop a workspace's cached model + frontmatter memo. Call after operations
@@ -1292,6 +1320,24 @@ export class AccessControlService {
1292
1320
  });
1293
1321
  return result;
1294
1322
  }
1323
+ async groupsOfUser(workspaceId, userEmail) {
1324
+ const model = await this.loadModel(workspaceId);
1325
+ const email = canonicalEmail(userEmail);
1326
+ const names = [];
1327
+ for (const principal of model.roles.byCanonical.values()) {
1328
+ if (principal.kind === 'group' && principal.emails.has(email))
1329
+ names.push(principal.displayName);
1330
+ }
1331
+ return names;
1332
+ }
1333
+ async visibleGroups(workspaceId, userEmail, groups) {
1334
+ if (groups.length === 0)
1335
+ return [];
1336
+ if (await this.holdsAdminRootWrite(workspaceId, userEmail))
1337
+ return [...groups];
1338
+ const mine = new Set(await this.groupsOfUser(workspaceId, userEmail));
1339
+ return groups.filter((g) => mine.has(g));
1340
+ }
1295
1341
  async canWriteBatch(workspaceId, userEmail, relativePaths) {
1296
1342
  const model = await this.loadModel(workspaceId);
1297
1343
  const repoDir = await this.repoDir(workspaceId);
@@ -1670,7 +1716,7 @@ export class AccessControlService {
1670
1716
  if (!loaded)
1671
1717
  return null;
1672
1718
  const repoDir = await this.repoDir(workspaceId);
1673
- const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath);
1719
+ const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath, loaded.tree);
1674
1720
  return hasPermissionResolved(loaded.model, 'write', userEmail, relativePath, own);
1675
1721
  }
1676
1722
  async canReadAtRef(workspaceId, ref, userEmail, relativePath) {
@@ -1678,7 +1724,7 @@ export class AccessControlService {
1678
1724
  if (!loaded)
1679
1725
  return null;
1680
1726
  const repoDir = await this.repoDir(workspaceId);
1681
- const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath);
1727
+ const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath, loaded.tree);
1682
1728
  return canReadResolved(loaded.model, userEmail, relativePath, own);
1683
1729
  }
1684
1730
  async canWriteBatchAtRef(workspaceId, ref, userEmail, relativePaths) {
@@ -1698,7 +1744,7 @@ export class AccessControlService {
1698
1744
  const repoDir = await this.repoDir(workspaceId);
1699
1745
  // One `git cat-file --batch` for the whole path set — a per-path `git
1700
1746
  // show` spawn made CR owner-routing take minutes on large change sets.
1701
- const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths);
1747
+ const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths, loaded.tree);
1702
1748
  const result = new Map();
1703
1749
  for (const p of relativePaths) {
1704
1750
  const machineOwned = machineAnswers.get(p);
@@ -1715,7 +1761,7 @@ export class AccessControlService {
1715
1761
  return null;
1716
1762
  const repoDir = await this.repoDir(workspaceId);
1717
1763
  // One `git cat-file --batch` for the whole set — see canWriteBatchAtRef.
1718
- const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths);
1764
+ const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths, loaded.tree);
1719
1765
  const result = new Map();
1720
1766
  for (const p of relativePaths) {
1721
1767
  result.set(p, canReadResolved(loaded.model, userEmail, p, owns.get(p) ?? null));
@@ -1734,7 +1780,7 @@ export class AccessControlService {
1734
1780
  if (!loaded)
1735
1781
  return null;
1736
1782
  const repoDir = await this.repoDir(workspaceId);
1737
- const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath);
1783
+ const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath, loaded.tree);
1738
1784
  return eligibleHoldersResolved(loaded.model, 'write', relativePath, own);
1739
1785
  }
1740
1786
  async holdsAdminRootWrite(workspaceId, userEmail) {
@@ -1826,7 +1872,7 @@ export class AccessControlService {
1826
1872
  return null;
1827
1873
  const repoDir = await this.repoDir(workspaceId);
1828
1874
  // One `git cat-file --batch` for the whole path set — see canWriteBatchAtRef.
1829
- const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths);
1875
+ const owns = await this.readOwnEntriesAtRefBatch(repoDir, loaded.resolvedRef, relativePaths, loaded.tree);
1830
1876
  for (const p of relativePaths) {
1831
1877
  if (result.has(p))
1832
1878
  continue;
@@ -2021,9 +2067,14 @@ export class AccessControlService {
2021
2067
  * grant themselves rights by editing a file's frontmatter in a branch the
2022
2068
  * gate isn't reading from.
2023
2069
  */
2024
- async readOwnEntriesAtRef(repoDir, ref, relativePath) {
2070
+ async readOwnEntriesAtRef(repoDir, ref, relativePath, tree = null) {
2025
2071
  if (relativePath === 'roles.yaml')
2026
2072
  return null;
2073
+ // At a commit (what every gate passes: `resolvedRef`), the batched read
2074
+ // and its memo answer; a single `git show` stays for a symbolic ref.
2075
+ if (isCommitSha(ref)) {
2076
+ return (await this.readOwnEntriesAtRefBatch(repoDir, ref, [relativePath], tree)).get(relativePath) ?? null;
2077
+ }
2027
2078
  const text = await this.showAtRef(repoDir, ref, relativePath);
2028
2079
  if (text === null)
2029
2080
  return null;
@@ -2039,13 +2090,31 @@ export class AccessControlService {
2039
2090
  * `access.md`/`roles.yaml` (which carry no own-entries) resolve to null,
2040
2091
  * mirroring the single-path helper.
2041
2092
  */
2042
- async readOwnEntriesAtRefBatch(repoDir, ref, relativePaths) {
2093
+ async readOwnEntriesAtRefBatch(repoDir, ref, relativePaths, tree = null) {
2043
2094
  const result = new Map();
2044
2095
  const wanted = [];
2096
+ const memo = isCommitSha(ref) ? this.ownEntriesAtCommit : null;
2045
2097
  for (const p of relativePaths) {
2046
- if (p === 'roles.yaml')
2098
+ if (p === 'roles.yaml') {
2099
+ result.set(p, null);
2100
+ continue;
2101
+ }
2102
+ const key = `${ref}\0${p}`;
2103
+ if (memo?.has(key)) {
2104
+ result.set(p, memo.get(key) ?? null);
2105
+ continue;
2106
+ }
2107
+ // A path the commit's tree does not carry has no own entries, as a
2108
+ // fact about the commit: remembered, and not read. (A change request
2109
+ // that adds a file asks about it at the base, where it does not exist
2110
+ // yet; one that deletes a file asks at the head.)
2111
+ if (tree && !tree.has(p)) {
2047
2112
  result.set(p, null);
2048
- else if (!wanted.includes(p))
2113
+ if (memo)
2114
+ this.rememberOwnEntriesAtCommit(key, null);
2115
+ continue;
2116
+ }
2117
+ if (!wanted.includes(p))
2049
2118
  wanted.push(p);
2050
2119
  }
2051
2120
  if (wanted.length === 0)
@@ -2053,13 +2122,30 @@ export class AccessControlService {
2053
2122
  const texts = await catFileBatch(this.gitRunner, repoDir, wanted.map((p) => `${ref}:${p}`));
2054
2123
  wanted.forEach((p, i) => {
2055
2124
  const text = texts[i];
2056
- if (text === null)
2125
+ if (text === null) {
2126
+ // Absent at the commit (when no tree was there to say so first), or
2127
+ // a blob git could not read: the batch says "missing" for both, and
2128
+ // only the first is a fact about the commit. Answered as no own
2129
+ // entries, as before, but never remembered.
2057
2130
  result.set(p, null);
2058
- else
2059
- result.set(p, isAccessMdPath(p) ? accessMdSelfEntries(text) : parseOwnAccessEntries(text));
2131
+ return;
2132
+ }
2133
+ const own = isAccessMdPath(p) ? accessMdSelfEntries(text) : parseOwnAccessEntries(text);
2134
+ result.set(p, own);
2135
+ // What a blob declares for itself — including nothing — is a fact about the commit.
2136
+ if (memo)
2137
+ this.rememberOwnEntriesAtCommit(`${ref}\0${p}`, own);
2060
2138
  });
2061
2139
  return result;
2062
2140
  }
2141
+ rememberOwnEntriesAtCommit(key, value) {
2142
+ this.ownEntriesAtCommit.set(key, value);
2143
+ if (this.ownEntriesAtCommit.size > AccessControlService.OWN_ENTRIES_AT_COMMIT_MAX) {
2144
+ const oldest = this.ownEntriesAtCommit.keys().next().value;
2145
+ if (oldest !== undefined)
2146
+ this.ownEntriesAtCommit.delete(oldest);
2147
+ }
2148
+ }
2063
2149
  /**
2064
2150
  * Resolve a candidate ref to one git accepts. Falls back to `origin/<ref>`
2065
2151
  * for short branch names, matching the heuristic in
@@ -2115,6 +2201,7 @@ export class AccessControlService {
2115
2201
  const { stdout } = await this.gitRunner.run(repoDir, ['ls-tree', '-r', '-z', '--name-only', ref]);
2116
2202
  const out = [];
2117
2203
  const pluginDirs = [];
2204
+ const all = [];
2118
2205
  // The fixed-name sources the tree carries (roles.yaml, the groups
2119
2206
  // files): a read of one of these that comes back empty is "absent"
2120
2207
  // only when the tree does not name it.
@@ -2123,6 +2210,9 @@ export class AccessControlService {
2123
2210
  for (const p of stdout.split('\0')) {
2124
2211
  if (!p)
2125
2212
  continue;
2213
+ // One past the cap is enough to know the tree is not kept.
2214
+ if (all.length <= this.treeIndexMaxPaths)
2215
+ all.push(p);
2126
2216
  if (p === 'access.md' || p.endsWith('/access.md'))
2127
2217
  out.push(p);
2128
2218
  else if (p === 'roles.yaml' || p === SYNCED_GROUPS_YAML || p === GROUPS_YAML)
@@ -2131,7 +2221,7 @@ export class AccessControlService {
2131
2221
  pluginDirs.push(p.slice(0, -(PLUGIN_MANIFEST_FILE.length + 1)));
2132
2222
  }
2133
2223
  }
2134
- return { accessFiles: out, pluginDirs, named };
2224
+ return { accessFiles: out, pluginDirs, named, all };
2135
2225
  }
2136
2226
  catch {
2137
2227
  return 'error';
@@ -2212,6 +2302,16 @@ export class AccessControlService {
2212
2302
  async revParseCommit(repoDir, ref) {
2213
2303
  if (!ref || ref.startsWith('-'))
2214
2304
  return null;
2305
+ // The ref files first (`readRevSha`): the names git would try, in git's
2306
+ // order, read without a process. "Missing" from the files is what
2307
+ // rev-parse answers nothing for; "unreadable" — a spelling the reader
2308
+ // does not resolve, a tag it cannot peel, a file it could not read — is
2309
+ // git's to decide, as the authority.
2310
+ const fromFiles = await readRevSha(repoDir, ref);
2311
+ if (fromFiles === 'missing')
2312
+ return null;
2313
+ if (fromFiles !== 'unreadable')
2314
+ return fromFiles.sha;
2215
2315
  try {
2216
2316
  const { stdout } = await this.gitRunner.run(repoDir, [
2217
2317
  'rev-parse',
@@ -2233,10 +2333,24 @@ export class AccessControlService {
2233
2333
  }
2234
2334
  rememberAtRef(key, value) {
2235
2335
  this.atRefCache.set(key, value);
2336
+ this.treePathsHeld += value.tree?.size ?? 0;
2236
2337
  if (this.atRefCache.size > AccessControlService.AT_REF_CACHE_MAX) {
2237
2338
  const oldest = this.atRefCache.keys().next().value;
2238
- if (oldest !== undefined)
2339
+ if (oldest !== undefined) {
2340
+ this.treePathsHeld -= this.atRefCache.get(oldest)?.tree?.size ?? 0;
2239
2341
  this.atRefCache.delete(oldest);
2342
+ }
2343
+ }
2344
+ // The trees are a budget of their own, across every held model: past
2345
+ // it, the oldest models keep their model and let their tree go — the
2346
+ // gate still answers, absence is just no longer remembered for them.
2347
+ for (const held of this.atRefCache.values()) {
2348
+ if (this.treePathsHeld <= this.treeIndexBudget)
2349
+ break;
2350
+ if (!held.tree)
2351
+ continue;
2352
+ this.treePathsHeld -= held.tree.size;
2353
+ held.tree = null;
2240
2354
  }
2241
2355
  }
2242
2356
  rememberNoRolesAtRef(key) {
@@ -2401,6 +2515,10 @@ export class AccessControlService {
2401
2515
  deploymentOwners: this.deploymentOwners,
2402
2516
  },
2403
2517
  resolvedRef: commit,
2518
+ // The listing already named every path; held so an own-entries read
2519
+ // can tell absence from a lost blob, unless the tree is too large to
2520
+ // hold for every cached commit (then absence is simply not remembered).
2521
+ tree: listed.all.length <= this.treeIndexMaxPaths ? new Set(listed.all) : null,
2404
2522
  };
2405
2523
  }
2406
2524
  }