@bevel-software/platform-core-backend 0.21.0 → 0.23.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 (434) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +62 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +22 -3
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +158 -23
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +34 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +89 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +0 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +11 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access-requests.contract.d.ts +75 -0
  31. package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
  32. package/dist/modules/access/access-requests.contract.js +20 -0
  33. package/dist/modules/access/access-requests.contract.js.map +1 -0
  34. package/dist/modules/access/access-requests.routes.d.ts +35 -0
  35. package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
  36. package/dist/modules/access/access-requests.routes.js +237 -0
  37. package/dist/modules/access/access-requests.routes.js.map +1 -0
  38. package/dist/modules/access/access-requests.service.d.ts +123 -0
  39. package/dist/modules/access/access-requests.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-requests.service.js +337 -0
  41. package/dist/modules/access/access-requests.service.js.map +1 -0
  42. package/dist/modules/access/access.routes.d.ts.map +1 -1
  43. package/dist/modules/access/access.routes.js +13 -15
  44. package/dist/modules/access/access.routes.js.map +1 -1
  45. package/dist/modules/access-model/access-grammar.d.ts +12 -0
  46. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  47. package/dist/modules/access-model/access-grammar.js +24 -0
  48. package/dist/modules/access-model/access-grammar.js.map +1 -1
  49. package/dist/modules/auth/account-admission.d.ts +58 -7
  50. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  51. package/dist/modules/auth/account-admission.js +44 -1
  52. package/dist/modules/auth/account-admission.js.map +1 -1
  53. package/dist/modules/auth/account.routes.d.ts +1 -1
  54. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  55. package/dist/modules/auth/account.routes.js +61 -5
  56. package/dist/modules/auth/account.routes.js.map +1 -1
  57. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  58. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  59. package/dist/modules/auth/auth.middleware.js +18 -7
  60. package/dist/modules/auth/auth.middleware.js.map +1 -1
  61. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  62. package/dist/modules/auth/auth.routes.js +8 -0
  63. package/dist/modules/auth/auth.routes.js.map +1 -1
  64. package/dist/modules/auth/auth.service.d.ts +96 -3
  65. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  66. package/dist/modules/auth/auth.service.js +187 -9
  67. package/dist/modules/auth/auth.service.js.map +1 -1
  68. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  69. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  70. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  71. package/dist/modules/database/core-schema.d.ts +41 -24
  72. package/dist/modules/database/core-schema.d.ts.map +1 -1
  73. package/dist/modules/database/core-schema.js +35 -27
  74. package/dist/modules/database/core-schema.js.map +1 -1
  75. package/dist/modules/github-app/github-app.client.d.ts +101 -0
  76. package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
  77. package/dist/modules/github-app/github-app.client.js +222 -0
  78. package/dist/modules/github-app/github-app.client.js.map +1 -0
  79. package/dist/modules/github-app/github-app.connection.d.ts +102 -0
  80. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
  81. package/dist/modules/github-app/github-app.connection.js +185 -0
  82. package/dist/modules/github-app/github-app.connection.js.map +1 -0
  83. package/dist/modules/github-app/github-app.routes.d.ts +59 -0
  84. package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
  85. package/dist/modules/github-app/github-app.routes.js +323 -0
  86. package/dist/modules/github-app/github-app.routes.js.map +1 -0
  87. package/dist/modules/github-app/index.d.ts +4 -0
  88. package/dist/modules/github-app/index.d.ts.map +1 -0
  89. package/dist/modules/github-app/index.js +4 -0
  90. package/dist/modules/github-app/index.js.map +1 -0
  91. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  92. package/dist/modules/kb-fs/branch-name.js +12 -2
  93. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  94. package/dist/modules/kb-fs/remote-url.d.ts +22 -0
  95. package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
  96. package/dist/modules/kb-fs/remote-url.js +35 -0
  97. package/dist/modules/kb-fs/remote-url.js.map +1 -0
  98. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  99. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  100. package/dist/modules/kb-fs/repo-path.js +79 -0
  101. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  102. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  104. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  105. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  106. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  108. package/dist/modules/kb-sync/sync-auth.js +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  110. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  112. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  113. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  114. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  115. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  116. package/dist/modules/plugins/join-proposals.d.ts +17 -5
  117. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  118. package/dist/modules/plugins/join-proposals.js +76 -22
  119. package/dist/modules/plugins/join-proposals.js.map +1 -1
  120. package/dist/modules/plugins/join-requests.service.d.ts +111 -18
  121. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-requests.service.js +173 -29
  123. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  124. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  125. package/dist/modules/plugins/plugins.routes.js +3 -2
  126. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  127. package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
  128. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  129. package/dist/modules/settings/deployment-settings.service.js +101 -7
  130. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  131. package/dist/modules/settings/managed-repository.d.ts +43 -0
  132. package/dist/modules/settings/managed-repository.d.ts.map +1 -0
  133. package/dist/modules/settings/managed-repository.js +60 -0
  134. package/dist/modules/settings/managed-repository.js.map +1 -0
  135. package/dist/modules/settings/repository-source.d.ts +128 -0
  136. package/dist/modules/settings/repository-source.d.ts.map +1 -0
  137. package/dist/modules/settings/repository-source.js +150 -0
  138. package/dist/modules/settings/repository-source.js.map +1 -0
  139. package/dist/modules/settings/setup.routes.d.ts +78 -4
  140. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  141. package/dist/modules/settings/setup.routes.js +379 -17
  142. package/dist/modules/settings/setup.routes.js.map +1 -1
  143. package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
  144. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  145. package/dist/modules/skills/skill-access-requests.routes.js +24 -63
  146. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  147. package/dist/modules/skills/skills.contract.d.ts +90 -18
  148. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  149. package/dist/modules/skills/skills.contract.js +4 -1
  150. package/dist/modules/skills/skills.contract.js.map +1 -1
  151. package/dist/modules/skills/skills.service.d.ts +98 -9
  152. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  153. package/dist/modules/skills/skills.service.js +239 -37
  154. package/dist/modules/skills/skills.service.js.map +1 -1
  155. package/dist/modules/skills/skills.tools.d.ts +7 -0
  156. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  157. package/dist/modules/skills/skills.tools.js +71 -7
  158. package/dist/modules/skills/skills.tools.js.map +1 -1
  159. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  160. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  161. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  162. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  163. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  164. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  165. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  166. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  167. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  168. package/dist/modules/tool-helpers/tool-context.js +15 -0
  169. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  170. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  171. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  172. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  173. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  174. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  175. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  176. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  177. package/dist/modules/tool-helpers/validate-token.js +1 -1
  178. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  179. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  180. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  181. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  182. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  183. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  184. package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
  185. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  186. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  187. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  188. package/dist/modules/workflow/file-lock.service.js +30 -0
  189. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  190. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  191. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  192. package/dist/modules/workflow/git/git.service.js +170 -6
  193. package/dist/modules/workflow/git/git.service.js.map +1 -1
  194. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  195. package/dist/modules/workflow/git/node-git-runner.js +5 -0
  196. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  197. package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
  198. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  199. package/dist/modules/workflow/git/pull-request.service.js +99 -0
  200. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  201. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  202. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  203. package/dist/modules/workflow/pending-commits.service.js +55 -0
  204. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  205. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  206. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  207. package/dist/modules/workflow/workflow-hooks.js +16 -1
  208. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  209. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  210. package/dist/modules/workflow/workflow.routes.js +3 -1
  211. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  212. package/dist/modules/workflow/workflow.service.d.ts +94 -13
  213. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  214. package/dist/modules/workflow/workflow.service.js +258 -51
  215. package/dist/modules/workflow/workflow.service.js.map +1 -1
  216. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  217. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  218. package/dist/modules/workspace/agent-access.gate.js +123 -0
  219. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  220. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  221. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  222. package/dist/modules/workspace/routine-write-policy.js +5 -6
  223. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  224. package/dist/modules/workspace/session-sink.d.ts +5 -5
  225. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  226. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  227. package/dist/modules/workspace/set-aside-clone.js +92 -0
  228. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  229. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
  230. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
  232. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +92 -3
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +272 -26
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  241. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  242. package/dist/modules/workspace/workspace.tools.js +337 -117
  243. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  244. package/dist/modules/write-access/write-access.d.ts +60 -0
  245. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  246. package/dist/modules/write-access/write-access.js +129 -0
  247. package/dist/modules/write-access/write-access.js.map +1 -0
  248. package/dist/shared/domain-errors.d.ts +64 -0
  249. package/dist/shared/domain-errors.d.ts.map +1 -1
  250. package/dist/shared/domain-errors.js +83 -0
  251. package/dist/shared/domain-errors.js.map +1 -1
  252. package/dist/shared/git.contract.d.ts +30 -0
  253. package/dist/shared/git.contract.d.ts.map +1 -1
  254. package/dist/shared/git.contract.js +26 -0
  255. package/dist/shared/git.contract.js.map +1 -1
  256. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  257. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  258. package/dist/tenancy/static-tenant-source.js +0 -2
  259. package/dist/tenancy/static-tenant-source.js.map +1 -1
  260. package/migrations/0014_change_request_closed_reason.sql +1 -0
  261. package/migrations/0015_account_deactivation.sql +3 -0
  262. package/migrations/meta/0014_snapshot.json +2265 -0
  263. package/migrations/meta/0015_snapshot.json +2271 -0
  264. package/migrations/meta/_journal.json +14 -0
  265. package/package.json +3 -3
  266. package/src/__tests__/kb-layout-config.test.ts +0 -2
  267. package/src/__tests__/retired-settings.test.ts +96 -0
  268. package/src/core/__tests__/gated-boot.test.ts +132 -0
  269. package/src/core/__tests__/lifecycle.test.ts +170 -1
  270. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  271. package/src/core/core-ports.ts +11 -1
  272. package/src/core/create-core-server.ts +69 -15
  273. package/src/core/create-core-services.ts +190 -27
  274. package/src/core/lifecycle.ts +98 -13
  275. package/src/core-config.ts +13 -14
  276. package/src/index.ts +12 -1
  277. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  278. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  279. package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
  280. package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
  281. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  282. package/src/modules/access/access-control.interface.ts +43 -12
  283. package/src/modules/access/access-control.service.ts +234 -36
  284. package/src/modules/access/access-requests.contract.ts +93 -0
  285. package/src/modules/access/access-requests.routes.ts +286 -0
  286. package/src/modules/access/access-requests.service.ts +420 -0
  287. package/src/modules/access/access.routes.ts +13 -15
  288. package/src/modules/access-model/access-grammar.ts +22 -0
  289. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  290. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  291. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  292. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  293. package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
  294. package/src/modules/auth/account-admission.ts +78 -9
  295. package/src/modules/auth/account.routes.ts +63 -7
  296. package/src/modules/auth/auth.middleware.ts +19 -8
  297. package/src/modules/auth/auth.routes.ts +8 -0
  298. package/src/modules/auth/auth.service.ts +207 -7
  299. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  300. package/src/modules/database/core-schema.ts +35 -27
  301. package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
  302. package/src/modules/github-app/github-app.client.ts +268 -0
  303. package/src/modules/github-app/github-app.connection.ts +204 -0
  304. package/src/modules/github-app/github-app.routes.ts +359 -0
  305. package/src/modules/github-app/index.ts +19 -0
  306. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  307. package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
  308. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  309. package/src/modules/kb-fs/branch-name.ts +14 -1
  310. package/src/modules/kb-fs/remote-url.ts +35 -0
  311. package/src/modules/kb-fs/repo-path.ts +85 -0
  312. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  313. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  314. package/src/modules/kb-sync/sync-auth.ts +2 -2
  315. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  316. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  317. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  318. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  319. package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
  320. package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
  321. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
  322. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  323. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  324. package/src/modules/plugins/join-proposals.ts +87 -19
  325. package/src/modules/plugins/join-requests.service.ts +199 -39
  326. package/src/modules/plugins/plugins.routes.ts +3 -2
  327. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  328. package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
  329. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
  330. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
  331. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
  332. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  333. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  334. package/src/modules/settings/deployment-settings.service.ts +117 -6
  335. package/src/modules/settings/managed-repository.ts +68 -0
  336. package/src/modules/settings/repository-source.ts +197 -0
  337. package/src/modules/settings/setup.routes.ts +463 -15
  338. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  339. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
  341. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  342. package/src/modules/skills/skill-access-requests.routes.ts +25 -75
  343. package/src/modules/skills/skills.contract.ts +91 -18
  344. package/src/modules/skills/skills.service.ts +278 -41
  345. package/src/modules/skills/skills.tools.ts +80 -8
  346. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  347. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  348. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  349. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  350. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  351. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  352. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  353. package/src/modules/tool-helpers/tool-context.ts +15 -0
  354. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  355. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  356. package/src/modules/tool-helpers/validate-token.ts +1 -1
  357. package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
  358. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  359. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  360. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
  361. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  362. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
  363. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
  364. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  365. package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
  366. package/src/modules/workflow/file-lock.service.ts +31 -0
  367. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  368. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
  369. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
  370. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
  371. package/src/modules/workflow/git/git.service.ts +193 -4
  372. package/src/modules/workflow/git/node-git-runner.ts +6 -0
  373. package/src/modules/workflow/git/pull-request.service.ts +119 -0
  374. package/src/modules/workflow/pending-commits.service.ts +59 -1
  375. package/src/modules/workflow/workflow-hooks.ts +64 -26
  376. package/src/modules/workflow/workflow.routes.ts +3 -1
  377. package/src/modules/workflow/workflow.service.ts +290 -55
  378. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  379. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  380. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  381. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  382. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  383. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  384. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  385. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  386. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  387. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  388. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
  389. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  390. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
  391. package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
  392. package/src/modules/workspace/agent-access.gate.ts +164 -0
  393. package/src/modules/workspace/routine-write-policy.ts +5 -6
  394. package/src/modules/workspace/session-sink.ts +5 -5
  395. package/src/modules/workspace/set-aside-clone.ts +96 -0
  396. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
  397. package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
  398. package/src/modules/workspace/workspace.routes.ts +95 -3
  399. package/src/modules/workspace/workspace.service.ts +273 -25
  400. package/src/modules/workspace/workspace.tools.ts +373 -119
  401. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  402. package/src/modules/write-access/write-access.ts +153 -0
  403. package/src/shared/domain-errors.ts +89 -0
  404. package/src/shared/git.contract.ts +37 -0
  405. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  406. package/src/tenancy/static-tenant-source.ts +0 -3
  407. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  408. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  409. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  410. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  411. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  412. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  413. package/dist/modules/workflow/session-ontology.service.js +0 -147
  414. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  415. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  416. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  417. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  418. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  419. package/dist/shared/kb-layout.d.ts +0 -39
  420. package/dist/shared/kb-layout.d.ts.map +0 -1
  421. package/dist/shared/kb-layout.js +0 -103
  422. package/dist/shared/kb-layout.js.map +0 -1
  423. package/dist/shared/kb-layout.test.d.ts +0 -2
  424. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  425. package/dist/shared/kb-layout.test.js +0 -75
  426. package/dist/shared/kb-layout.test.js.map +0 -1
  427. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  428. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  429. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  430. package/src/modules/workflow/session-ontology.service.ts +0 -183
  431. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  432. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  433. package/src/shared/kb-layout.test.ts +0 -98
  434. package/src/shared/kb-layout.ts +0 -102
@@ -84,11 +84,14 @@ export type HolderList = {
84
84
  export type PathHolders = { read: HolderList; write: HolderList };
85
85
 
86
86
  /**
87
- * One file's holders where it is now and where a move would put it — see
88
- * {@link IAccessControl.prospectiveHolders}.
87
+ * One file's or folder's holders where it is now and where a move would put
88
+ * it — see {@link IAccessControl.prospectiveHolders}.
89
89
  */
90
90
  export type ProspectiveHolders = { before: PathHolders; after: PathHolders };
91
91
 
92
+ /** One caller's own verdict per access verb on one path. */
93
+ export type PathVerbs = { read: boolean; write: boolean; download: boolean; owner: boolean };
94
+
92
95
  /**
93
96
  * Per-verb sources of a principal's access on a target. Only verbs the principal
94
97
  * actually holds (via a named file entry) appear; each maps to the closest-first
@@ -358,19 +361,17 @@ export interface IAccessControl {
358
361
  }>;
359
362
 
360
363
  /**
361
- * Who can open and who can edit one file where it IS, and where a move
362
- * would put it. `toPath` names a path that does not exist yet — the point
363
- * of the call is to answer before the move happens — so the resolution
364
- * layers the file's OWN rules (its frontmatter, read from `fromPath`,
365
- * which travels with the bytes) over the destination's folder chain.
364
+ * Who can open and who can edit one file or folder where it IS, and where a
365
+ * move would put it. `toPath` names a path that does not exist yet — the
366
+ * point of the call is to answer before the move happens — so the
367
+ * resolution is over what travels with the bytes: a file's OWN rules (its
368
+ * frontmatter, read from `fromPath`) layered over the destination's folder
369
+ * chain, or a folder's `access.md` files keyed at the directories they
370
+ * land in. Either way the rules the source inherited from its old parent
371
+ * are left behind and the new parent's apply.
366
372
  *
367
373
  * Writes nothing and moves nothing. The move confirmation diffs the two
368
374
  * sides to name who loses and who gains access.
369
- *
370
- * A FILE question only: a `fromPath` that is a directory is refused with a
371
- * 400. A folder's access is its own `access.md` — which moves with it and
372
- * governs everything beneath it — so resolving it as a file would name the
373
- * wrong principals with the same confidence as the right ones.
374
375
  */
375
376
  prospectiveHolders(
376
377
  workspaceId: string,
@@ -378,6 +379,36 @@ export interface IAccessControl {
378
379
  toPath: string,
379
380
  ): Promise<ProspectiveHolders>;
380
381
 
382
+ /**
383
+ * The CALLER'S OWN `{ read, write, download, owner }` at `toPath` as they
384
+ * will be once `fromPath` has been moved (`sourceRemains: false`, the
385
+ * default) or copied (`sourceRemains: true`) there — the question a move's
386
+ * or copy's dry run asks, which the same four gates at `toPath` cannot
387
+ * answer because what travels with the bytes is not at the destination yet:
388
+ * the `access.md` files inside a folder, keyed at the directories they land
389
+ * in, and a file's own frontmatter.
390
+ *
391
+ * What a single file's rules GOVERN follows the name it lands under, not the
392
+ * one it leaves: landing as a directory's `access.md` they are that
393
+ * directory's rules (so copying `Sales/access.md` to `Legal/access.md`
394
+ * previews the access `Legal/` is about to have), and landing under any
395
+ * other name they are the file's own and govern nothing but it. Either way
396
+ * the answer is the one the gates will give a moment later.
397
+ *
398
+ * PREVIEW ONLY. It describes a tree that does not exist, so it must never
399
+ * decide whether an operation may run: the gates keep resolving against the
400
+ * tree that does. It writes nothing and moves nothing, and it answers the
401
+ * caller's own verbs only — never the content of an `access.md` they may
402
+ * not read.
403
+ */
404
+ previewAccessAfterRelocation(
405
+ workspaceId: string,
406
+ userEmail: string,
407
+ fromPath: string,
408
+ toPath: string,
409
+ opts?: { sourceRemains?: boolean },
410
+ ): Promise<PathVerbs>;
411
+
381
412
  /**
382
413
  * Finite expanded email set for configured users who could approve this path
383
414
  * — role members + direct user grants, minus anyone denied. The built-in
@@ -5,7 +5,7 @@ import { logger } from '../../shared/logging.js';
5
5
  const log = logger('access');
6
6
 
7
7
  import { isAbsence, type ITreeWalker, type WalkListener } from '../../shared/fs.contract.js';
8
- import { isGitTimeout, type IGitRunner } from '../../shared/git.contract.js';
8
+ import { assertOneSpecPerLine, isGitTimeout, type IGitRunner } from '../../shared/git.contract.js';
9
9
  import { hashEmail } from '../../shared/email-identity.js';
10
10
  import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
11
11
  import type { WorkspaceService } from '../workspace/workspace.service.js';
@@ -19,6 +19,7 @@ import type {
19
19
  GrantSource,
20
20
  GrantSources,
21
21
  PathHolders,
22
+ PathVerbs,
22
23
  ProspectiveHolders,
23
24
  ResolvedPrincipal,
24
25
  } from './access-control.interface.js';
@@ -31,7 +32,6 @@ import {
31
32
  } from '@bevel-software/platform-shared';
32
33
  import type { KbContext } from '../../shared/kb-context.js';
33
34
  import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
34
- import { WorkflowDomainError } from '../../shared/domain-errors.js';
35
35
  import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
36
36
  import {
37
37
  GROUPS_YAML,
@@ -81,6 +81,12 @@ async function catFileBatch(
81
81
  ): Promise<(string | null)[]> {
82
82
  // Bytes, not text: every `<size>` in the header counts bytes, and the walk
83
83
  // below has to step by that count through content that is not all ASCII.
84
+ // One spec per LINE is the protocol, and this walk depends on getting back
85
+ // exactly as many answers as it sent: a spec carrying a line break would
86
+ // insert an extra answer and shift every result after it, so each directory
87
+ // would be handed the NEXT one's `access.md`. Paths are refused a control
88
+ // character at the normaliser; this is the fence at the protocol.
89
+ assertOneSpecPerLine(specs);
84
90
  const { stdout: out } = await runner.run(repoDir, ['cat-file', '--batch'], {
85
91
  input: `${specs.join('\n')}\n`,
86
92
  encoding: 'buffer',
@@ -951,6 +957,95 @@ function resolveDenialSourcesForVerb(
951
957
  return out;
952
958
  }
953
959
 
960
+ /**
961
+ * The model as it will be once `fromPath` has been moved or copied to
962
+ * `toPath`: every `access.md` the operation carries, keyed at the directory
963
+ * it lands in, since the rules a folder carries travel with its bytes.
964
+ *
965
+ * What travels is BYTES, and the name they land under decides what they
966
+ * govern — `landing` is the caller's answer to that (see
967
+ * `relocationSubject`), already parsed as the destination path:
968
+ *
969
+ * - a FOLDER takes every `access.md` at or under it, each re-keyed to the
970
+ * directory it lands in, and `landing` plays no part;
971
+ * - a FILE governs a directory exactly when it lands as that directory's
972
+ * `access.md`, whatever it was called before — a copy `copy_file` allows,
973
+ * and one that governs the destination folder the moment it lands. Its key
974
+ * is a PARENT directory, never a path under `fromPath`, which is why the
975
+ * prefix walk above cannot see it.
976
+ *
977
+ * The emptied side is the mirror: an `access.md` that MOVES stops governing
978
+ * the directory it left, whether or not it governs anything where it lands.
979
+ *
980
+ * PREVIEW ONLY. Nothing here reads disk and nothing here decides whether an
981
+ * operation may run — the gates resolve against the model as it IS. This
982
+ * answers the other question, the one a dry run asks: what will be true
983
+ * afterwards. Judging the destination as it stands today is what made a
984
+ * rename warn about losing the owner access the folder's own `access.md` was
985
+ * about to hand straight back.
986
+ *
987
+ * `sourceRemains` is the difference between a copy and a move: a copy leaves
988
+ * the rules where they were, a move takes them away. It changes the answer
989
+ * only when the destination resolves through a directory the source
990
+ * occupied; nothing else consults those keys.
991
+ */
992
+ function withRelocatedAccessFiles(
993
+ model: AccessModel,
994
+ fromPath: string,
995
+ toPath: string,
996
+ opts: { sourceRemains: boolean; sourceIsFolder: boolean; landing?: AccessFile | null },
997
+ ): AccessModel {
998
+ /** Directories the operation takes an `access.md` AWAY from. */
999
+ const emptied: string[] = [];
1000
+ /** Directories it leaves one IN, with the file as it will be read there. */
1001
+ const landing: { toDir: string; file: AccessFile }[] = [];
1002
+ if (opts.sourceIsFolder) {
1003
+ for (const [dir, file] of model.accessFilesByDir) {
1004
+ if (dir !== fromPath && !dir.startsWith(`${fromPath}/`)) continue;
1005
+ emptied.push(dir);
1006
+ landing.push({ toDir: `${toPath}${dir.slice(fromPath.length)}`, file });
1007
+ }
1008
+ } else {
1009
+ const fromDir = parentDirOf(fromPath);
1010
+ if (isAccessMdPath(fromPath) && model.accessFilesByDir.has(fromDir)) emptied.push(fromDir);
1011
+ if (opts.landing) landing.push({ toDir: parentDirOf(toPath), file: opts.landing });
1012
+ }
1013
+ if (emptied.length === 0 && landing.length === 0) return model;
1014
+ const accessFilesByDir = new Map(model.accessFilesByDir);
1015
+ if (!opts.sourceRemains) for (const dir of emptied) accessFilesByDir.delete(dir);
1016
+ for (const { toDir, file } of landing) {
1017
+ // A directory that already has an `access.md` keeps it. The operation
1018
+ // would be refused for landing on something that exists long before the
1019
+ // two files could meet, so the model must not invent a merge either.
1020
+ if (accessFilesByDir.has(toDir)) continue;
1021
+ accessFilesByDir.set(toDir, {
1022
+ ...file,
1023
+ dir: toDir,
1024
+ path: toDir ? `${toDir}/access.md` : 'access.md',
1025
+ });
1026
+ }
1027
+ return { ...model, accessFilesByDir };
1028
+ }
1029
+
1030
+ /** The directory a repo-relative file path sits in; `''` for the repo root. */
1031
+ function parentDirOf(relativePath: string): string {
1032
+ const cut = relativePath.lastIndexOf('/');
1033
+ return cut === -1 ? '' : relativePath.slice(0, cut);
1034
+ }
1035
+
1036
+ /**
1037
+ * One file's own-entries, parsed as the path `asName` would be read.
1038
+ *
1039
+ * The same frontmatter means two different things by name: in an `access.md`
1040
+ * it is the FOLDER's rules (and the file governs itself only through the
1041
+ * body format — see {@link accessMdSelfEntries}), and anywhere else it is the
1042
+ * file's own. Which is why a preview must read the bytes as the name they
1043
+ * LAND under, not the one they are leaving.
1044
+ */
1045
+ function ownEntriesFromText(text: string, asName: string): OwnEntries | null {
1046
+ return isAccessMdPath(asName) ? accessMdSelfEntries(text) : parseOwnAccessEntries(text);
1047
+ }
1048
+
954
1049
  /**
955
1050
  * Build the repo-root → path chain of directory scopes that govern
956
1051
  * `relativePath`, e.g. `Knowledge/Sales/Foo.md` →
@@ -1586,16 +1681,25 @@ export class AccessControlService implements IAccessControl {
1586
1681
  }
1587
1682
 
1588
1683
  /**
1589
- * The read and write holders of one file at its current path and at a path
1590
- * it has not moved to yet (see the interface).
1684
+ * The read and write holders of one file or folder at its current path and
1685
+ * at a path it has not moved to yet (see the interface).
1686
+ *
1687
+ * A move changes nothing about what the thing itself carries — a file's
1688
+ * frontmatter and a folder's `access.md` files travel with the bytes — and
1689
+ * everything about the folder chain above it. So the hypothetical side is
1690
+ * the ordinary resolution at the destination path, over what travels:
1591
1691
  *
1592
- * A move changes nothing about the file's own frontmatter — that travels
1593
- * with the bytes — and everything about the folder chain above it. So the
1594
- * hypothetical side is the ordinary resolution with the destination path
1595
- * substituted: the same model, the same own-entries (read from the SOURCE,
1596
- * since nothing sits at the destination to read), resolved against the
1597
- * destination's ancestors. One model load and one file read serve all four
1598
- * lookups.
1692
+ * - a FILE's own-entries, read from the SOURCE (nothing sits at the
1693
+ * destination to read) and parsed as the name they LAND under, layered
1694
+ * over the destination's ancestors — the two sides can differ, because
1695
+ * the same frontmatter is a folder's rules in an `access.md` and the
1696
+ * file's own anywhere else;
1697
+ * - a FOLDER's `access.md` files, keyed at the directories they land in,
1698
+ * and a file landing as a directory's `access.md` at the directory it
1699
+ * lands in (`withRelocatedAccessFiles`). A folder has no frontmatter of
1700
+ * its own, so it passes none.
1701
+ *
1702
+ * One model load and one file read serve all four lookups.
1599
1703
  */
1600
1704
  async prospectiveHolders(
1601
1705
  workspaceId: string,
@@ -1604,31 +1708,64 @@ export class AccessControlService implements IAccessControl {
1604
1708
  ): Promise<ProspectiveHolders> {
1605
1709
  const model = await this.loadModel(workspaceId);
1606
1710
  const repoDir = await this.repoDir(workspaceId);
1607
- // A FILE question only. A folder carries its own `access.md` — which moves
1608
- // with it and governs everything under it — so resolving it as a file
1609
- // would read frontmatter it does not have and omit the rules it does,
1610
- // naming principals that are not the ones a folder move changes. Refuse
1611
- // rather than answer the wrong question convincingly.
1612
- //
1613
- // Only absence is an answer here (see `isAbsence`): a probe that failed on
1614
- // permissions or I/O does not say "this is a file", and folding it into
1615
- // one would let the very case above through on an unreadable source.
1616
- const fromStat = await fs.stat(path.join(repoDir, fromPath)).catch((err: unknown) => {
1617
- if (isAbsence(err)) return null;
1618
- throw err;
1711
+ const { isFolder, ownHere, ownThere, landing } =
1712
+ await this.relocationSubject(repoDir, fromPath, toPath);
1713
+ const after = withRelocatedAccessFiles(model, fromPath, toPath, {
1714
+ sourceRemains: false,
1715
+ sourceIsFolder: isFolder,
1716
+ landing,
1619
1717
  });
1620
- if (fromStat?.isDirectory()) {
1621
- throw new WorkflowDomainError(
1622
- 'prospective access answers for a file, not a folder',
1623
- 400,
1624
- );
1625
- }
1626
- const own = await this.readOwnEntries(repoDir, fromPath);
1627
- const holdersAt = (relativePath: string): PathHolders => ({
1628
- read: eligibleHoldersResolved(model, 'read', relativePath, own),
1629
- write: eligibleHoldersResolved(model, 'write', relativePath, own),
1718
+ const holdersAt = (m: AccessModel, relativePath: string, own: OwnEntries | null): PathHolders => ({
1719
+ read: eligibleHoldersResolved(m, 'read', relativePath, own),
1720
+ write: eligibleHoldersResolved(m, 'write', relativePath, own),
1630
1721
  });
1631
- return { before: holdersAt(fromPath), after: holdersAt(toPath) };
1722
+ return {
1723
+ before: holdersAt(model, fromPath, ownHere),
1724
+ after: holdersAt(after, toPath, ownThere),
1725
+ };
1726
+ }
1727
+
1728
+ /**
1729
+ * The caller's own verbs at `toPath` as they will be once `fromPath` has
1730
+ * been moved or copied there (see the interface).
1731
+ *
1732
+ * Identical to asking `canRead`/`canWrite`/`canDownload`/`canOwner` at
1733
+ * `toPath` whenever nothing travels — an ordinary file with no rules of its
1734
+ * own, or a folder with no `access.md` of its own — so the cases this was
1735
+ * not written for answer exactly as they always did.
1736
+ *
1737
+ * What travels is what `prospectiveHolders` says travels, and for the same
1738
+ * reason: the preview and the dialog's holder lists must not disagree about
1739
+ * one move. A file's own-entries come from the SOURCE, where the bytes
1740
+ * carrying them still are — asking the destination, which does not exist
1741
+ * yet, always answered null and so dropped the frontmatter a rename hands
1742
+ * straight back (requirement 6: what the caller has afterwards equals what
1743
+ * the preview said) — and they are parsed as the name they LAND under,
1744
+ * because that is the parse the gates will use a moment later.
1745
+ */
1746
+ async previewAccessAfterRelocation(
1747
+ workspaceId: string,
1748
+ userEmail: string,
1749
+ fromPath: string,
1750
+ toPath: string,
1751
+ opts?: { sourceRemains?: boolean },
1752
+ ): Promise<PathVerbs> {
1753
+ const repoDir = await this.repoDir(workspaceId);
1754
+ const { isFolder, ownThere: own, landing } =
1755
+ await this.relocationSubject(repoDir, fromPath, toPath);
1756
+ const model = withRelocatedAccessFiles(
1757
+ await this.loadModel(workspaceId),
1758
+ fromPath,
1759
+ toPath,
1760
+ { sourceRemains: opts?.sourceRemains === true, sourceIsFolder: isFolder, landing },
1761
+ );
1762
+ const machineOwned = this.machineOwnedWriteRule(userEmail, toPath);
1763
+ return {
1764
+ read: canReadResolved(model, userEmail, toPath, own),
1765
+ write: machineOwned ?? hasPermissionResolved(model, 'write', userEmail, toPath, own),
1766
+ download: hasPermissionResolved(model, 'download', userEmail, toPath, own),
1767
+ owner: hasPermissionResolved(model, 'owner', userEmail, toPath, own),
1768
+ };
1632
1769
  }
1633
1770
 
1634
1771
  async eligibleWriterEmails(
@@ -2219,8 +2356,69 @@ export class AccessControlService implements IAccessControl {
2219
2356
  } catch {
2220
2357
  return null;
2221
2358
  }
2222
- if (isAccessMdPath(relativePath)) return accessMdSelfEntries(text);
2223
- return parseOwnAccessEntries(text);
2359
+ return ownEntriesFromText(text, relativePath);
2360
+ }
2361
+
2362
+ /**
2363
+ * What a move or a copy of `fromPath` to `toPath` CARRIES — the one disk
2364
+ * read both preview surfaces need:
2365
+ *
2366
+ * - `isFolder`: the source is a folder, so the `access.md` files under it
2367
+ * travel, keyed at the directories they land in;
2368
+ * - `ownHere` / `ownThere`: a file's own-entries as the SOURCE's name reads
2369
+ * them and as the DESTINATION's name will. They differ because the same
2370
+ * frontmatter means two things by name (see `ownEntriesFromText`), so
2371
+ * `Sales/access.md` copied to `Legal/rules-copy.md` arrives as an
2372
+ * ordinary file whose frontmatter is its own — reading it as the source's
2373
+ * name had the preview contradict the copy it was previewing;
2374
+ * - `landing`: the folder rules the bytes WILL be, when they land under the
2375
+ * name `access.md`. Parsed as the destination path, so the directory it
2376
+ * governs is already the right one. An `access.md` that will not parse
2377
+ * travels as nothing, as the loader treats it.
2378
+ *
2379
+ * `lstat`, not `stat`: a SYMLINK is none of this. `move_file` refuses a
2380
+ * symlink source outright, so a link to a directory must not be previewed
2381
+ * as a folder taking access files along, and nothing is read THROUGH the
2382
+ * link either — the link's target is not what would move.
2383
+ *
2384
+ * Only absence is an answer (see `isAbsence`): a probe that failed on
2385
+ * permissions or I/O does not say "this is a file", and reading a folder as
2386
+ * one would resolve it through frontmatter it does not have.
2387
+ */
2388
+ private async relocationSubject(
2389
+ repoDir: string,
2390
+ fromPath: string,
2391
+ toPath: string,
2392
+ ): Promise<{
2393
+ isFolder: boolean;
2394
+ ownHere: OwnEntries | null;
2395
+ ownThere: OwnEntries | null;
2396
+ landing: AccessFile | null;
2397
+ }> {
2398
+ const nothing = { isFolder: false, ownHere: null, ownThere: null, landing: null };
2399
+ const fromStat = await fs.lstat(path.join(repoDir, fromPath)).catch((err: unknown) => {
2400
+ if (isAbsence(err)) return null;
2401
+ throw err;
2402
+ });
2403
+ if (fromStat?.isSymbolicLink() === true) return nothing;
2404
+ if (fromStat?.isDirectory() === true) return { ...nothing, isFolder: true };
2405
+ // `roles.yaml` carries no own-entries wherever it sits, exactly as
2406
+ // `readOwnEntries` has it; a read that fails for any reason is no rules,
2407
+ // which is what the gates would say about the same file.
2408
+ if (fromPath === 'roles.yaml' || toPath === 'roles.yaml') return nothing;
2409
+ let text: string;
2410
+ try {
2411
+ text = await fs.readFile(path.join(repoDir, fromPath), 'utf-8');
2412
+ } catch {
2413
+ return nothing;
2414
+ }
2415
+ const parsed = isAccessMdPath(toPath) ? parseAccessFile(text, toPath) : null;
2416
+ return {
2417
+ isFolder: false,
2418
+ ownHere: ownEntriesFromText(text, fromPath),
2419
+ ownThere: ownEntriesFromText(text, toPath),
2420
+ landing: parsed?.ok === true ? parsed.file : null,
2421
+ };
2224
2422
  }
2225
2423
 
2226
2424
  /**
@@ -0,0 +1,93 @@
1
+ import type { AuthUser, ChangeRequest } from '@bevel-software/platform-shared';
2
+ import type { Verb } from '../access-model/access-grammar.js';
3
+ import { accessMdPathForFolder, type TargetKind } from './access-mutation.service.js';
4
+
5
+ /**
6
+ * The ITEM a request is about — a folder, or a file that carries its own
7
+ * rules. Everything downstream (the branch name, the file spliced, the
8
+ * permission checked) is derived from this pair, so a request on a file can
9
+ * never reach beyond that file.
10
+ */
11
+ export interface AccessRequestTarget {
12
+ /** Repo-relative path of the item itself — NOT of its rules file. */
13
+ path: string;
14
+ kind: TargetKind;
15
+ }
16
+
17
+ /** A folder target, spelled once. */
18
+ export function folderTarget(repoRelFolder: string): AccessRequestTarget {
19
+ return { path: repoRelFolder, kind: 'folder' };
20
+ }
21
+
22
+ /**
23
+ * The file whose rules a request on this item edits: a folder's `access.md`,
24
+ * or the file itself (its own frontmatter). The one place the two cases part,
25
+ * so a file request writes nothing a sibling can see.
26
+ */
27
+ export function rulesFileFor(target: AccessRequestTarget): string {
28
+ return target.kind === 'folder' ? accessMdPathForFolder(target.path) : target.path;
29
+ }
30
+
31
+ /** The levels the Manage access dialog can ask for. `owner` covers `write`. */
32
+ export type RequestLevel = Extract<Verb, 'write' | 'owner'>;
33
+ export const REQUEST_LEVELS: readonly RequestLevel[] = ['write', 'owner'];
34
+ export function isRequestLevel(value: unknown): value is RequestLevel {
35
+ return typeof value === 'string' && (REQUEST_LEVELS as readonly string[]).includes(value);
36
+ }
37
+
38
+ /** The optional note a requester may attach, in characters. */
39
+ export const REQUEST_NOTE_MAX = 500;
40
+
41
+ /**
42
+ * One grant a request's branch still proposes. Structurally the plugin
43
+ * module's `JoinProposal` — stated here so the access module can consume the
44
+ * request lifecycle without importing the module that implements it (the
45
+ * dependency runs library → access, and this keeps it that way).
46
+ */
47
+ export interface AccessProposal {
48
+ verb: Verb;
49
+ id: string;
50
+ principal:
51
+ | { kind: 'user'; email: string; displayName: string }
52
+ | { kind: 'role'; role: string };
53
+ label: string;
54
+ }
55
+
56
+ /** One open request, as the editors' surfaces render it. */
57
+ export interface AccessRequestSummary {
58
+ number: number;
59
+ branch: string;
60
+ requesterName: string;
61
+ createdAt: string;
62
+ proposals: AccessProposal[];
63
+ /** The requester's note, as plain text, when they gave one. */
64
+ note?: string;
65
+ }
66
+
67
+ /**
68
+ * The request lifecycle, as the access module needs it: what a branch still
69
+ * proposes, the open requests for an item, and the settle-if-nothing-is-left
70
+ * check. Implemented by `JoinRequestsService`.
71
+ *
72
+ * `key` is what `joinBranchFor` was given when the branch was named — a
73
+ * plugin's name for a Subscribe request, and the ITEM'S OWN PATH for every
74
+ * request that comes from the Manage access dialog. Keying a skill folder's
75
+ * request on its path is what makes the skill page's "Request write access"
76
+ * and a Can edit request on that same folder one request rather than two.
77
+ */
78
+ export interface IAccessRequestLifecycle {
79
+ /** Null ⇒ the branch could not be read; that is not "proposes nothing". */
80
+ proposalsOn(branch: string, target: AccessRequestTarget): Promise<AccessProposal[] | null>;
81
+ list(
82
+ key: string,
83
+ target: AccessRequestTarget,
84
+ crs: ChangeRequest[],
85
+ actor: AuthUser,
86
+ ): Promise<AccessRequestSummary[]>;
87
+ reconcile(
88
+ key: string,
89
+ target: AccessRequestTarget,
90
+ cr: ChangeRequest,
91
+ actor: AuthUser,
92
+ ): Promise<boolean>;
93
+ }