@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
@@ -31,8 +31,27 @@ import type { KbContext } from '../../shared/kb-context.js';
31
31
  import { failureOf, type GitFailure } from '../../shared/git-failure.js';
32
32
  import { redactSecret, urlQuerySecrets } from '../../shared/redact-secret.js';
33
33
  import { listRootFolders, pickListingBranch } from './git-root-folders.js';
34
+ import { sameRepository } from '../kb-fs/remote-url.js';
35
+ import { MANAGED_DEFAULT_BRANCH } from './managed-repository.js';
36
+ import { GIT_MODES, type GitHubAppRepository, type RepositorySource } from './repository-source.js';
34
37
  import '../auth/auth.middleware.js'; // Express Request augmentation
35
38
 
39
+ /**
40
+ * What setup needs of the ways a deployment can be given its repository:
41
+ * the source every reader of the repository asks, and the one thing a mode
42
+ * has to have DONE before a save that chooses it may go through.
43
+ */
44
+ export interface RepositorySetup {
45
+ source: RepositorySource;
46
+ /** Create the repository the deployment keeps for itself, if it is not there. Never touches one that is. */
47
+ ensureManaged(initialBranch: string): Promise<void>;
48
+ /**
49
+ * The connection to GitHub through a GitHub App. Absent, that way is not
50
+ * offered, and a save that names it is refused.
51
+ */
52
+ githubApp?: Pick<GitHubAppRepository, 'url' | 'answered' | 'prepare' | 'token' | 'permits'>;
53
+ }
54
+
36
55
  /** Which name a host expects beside the token when none is configured. */
37
56
  const DEFAULT_GIT_USERNAME = 'x-access-token';
38
57
 
@@ -72,6 +91,49 @@ export interface LastSyncStatus {
72
91
  results: Array<{ branch: string; outcome: string; error?: string }>;
73
92
  }
74
93
 
94
+ /**
95
+ * The open change requests, as the repository-change confirmation needs them:
96
+ * how many there are to decide about, and the close the admin may choose.
97
+ *
98
+ * Deliberately narrow — the setup routes never read a request, never write
99
+ * one, and never learn what is in it. Nothing here deletes anything: closing
100
+ * leaves every row where it is.
101
+ */
102
+ export interface RepositoryChangeRequests {
103
+ /** How many change requests are open right now. */
104
+ countOpen(): Promise<number>;
105
+ /** Close every open request as "repository replaced", releasing the file locks on their branches. Answers how many closed. */
106
+ closeAsRepositoryReplaced(): Promise<number>;
107
+ /**
108
+ * Run the move with no queued commit being written. The startup phase sets
109
+ * working copies aside and clones them again while the deployment is
110
+ * serving, and a commit worker left running would write into a directory
111
+ * that is being renamed away, or into the fresh clone of a repository the
112
+ * commit was never meant for. Absent (a minimal mount has no worker), the
113
+ * move runs as it is.
114
+ */
115
+ whileCommitsHeld?<T>(work: () => Promise<T>): Promise<T>;
116
+ }
117
+
118
+ /**
119
+ * What the admin is asked to confirm before a save MOVES the deployment to
120
+ * another repository, and what they answered about its open change requests.
121
+ *
122
+ * `keep` — the repository only moved: its branches came along, so the open
123
+ * change requests still mean something and stay open.
124
+ * `close` — it is a different repository: none of those branches is in it,
125
+ * so the requests are closed as "repository replaced" (nothing deleted).
126
+ *
127
+ * What happens to the working copies is not a choice, and is decided by the
128
+ * histories, not by this answer (see the startup phase's
129
+ * `reconcileClonesWithConfiguredRepository`): a copy whose history the new
130
+ * repository holds is pointed at it and kept, unpushed commits included; any
131
+ * other is set aside whole and cloned fresh. Nothing is deleted. The
132
+ * confirmation exists so that work which leaves the app this way does so
133
+ * KNOWINGLY.
134
+ */
135
+ export type RepositoryChangeChoice = 'keep' | 'close';
136
+
75
137
  /**
76
138
  * First-run setup — the deployment's own configuration, for the things that
77
139
  * used to be environment-only.
@@ -138,8 +200,24 @@ export function createSetupRoutes(
138
200
  checkOidc: (config: OidcConfiguration) => Promise<OidcCheck> = checkOidcConfiguration,
139
201
  /** The issuer-only half, for a test with no application id or secret yet. */
140
202
  checkIssuer: (issuerUrl: string) => Promise<IssuerCheck> = (url) => checkOidcIssuer(url),
203
+ /**
204
+ * The ways a deployment can be given its repository. Absent, there is the
205
+ * one there always was: an address and a token, read from the settings.
206
+ */
207
+ repository?: RepositorySetup,
208
+ /**
209
+ * What a change of the knowledge-base repository has to decide about, and
210
+ * what to do when the admin decides to close.
211
+ *
212
+ * Injected because the setup routes must stay free of the workflow module —
213
+ * and a minimal mount (tests, a distribution without change requests) simply
214
+ * has none: the default answers "no open requests", so the confirmation
215
+ * offers no choice it cannot honour.
216
+ */
217
+ changeRequests: RepositoryChangeRequests = { async countOpen() { return 0; }, async closeAsRepositoryReplaced() { return 0; } },
141
218
  ): express.Router {
142
219
  const router = express.Router();
220
+ const source = repository?.source;
143
221
 
144
222
  /**
145
223
  * Why the last setup-time run of the KB startup phase FAILED, or null. While
@@ -184,7 +262,30 @@ export function createSetupRoutes(
184
262
  };
185
263
  /** The app-gate answer: settings complete AND the KB phase settled clean, whoever ran it. */
186
264
  const kbReady = () =>
187
- isComplete(settings, kb) && kbInit === null && kbInitInFlight === null && bootFailure() === null;
265
+ isComplete(settings, kb, source) && kbInit === null && kbInitInFlight === null && bootFailure() === null;
266
+
267
+ /**
268
+ * Which ways of having a repository this deployment offers and which one
269
+ * it is on, for the setup screen. `mode` is the one IN EFFECT, inferred
270
+ * for a deployment that never chose; `chosen` is the one the settings
271
+ * say, which differs from it between a move and the restart the move
272
+ * owes. The screen opens on what was chosen, says a restart is pending
273
+ * when the two differ, and offers the first of `modes` to a deployment
274
+ * that has none. `pinned` names the variable that chose for the
275
+ * deployment, when one did. Nothing at all from a mount without the
276
+ * choice, which the screen reads as the one way there was.
277
+ */
278
+ const repositoryStatus = () =>
279
+ source
280
+ ? {
281
+ repository: {
282
+ mode: source.mode(),
283
+ chosen: source.chosen(),
284
+ ...(settings.sourceOf('gitMode') === 'env' ? { pinned: 'GIT_MODE' } : {}),
285
+ modes: GIT_MODES.filter((mode) => mode !== 'github-app' || repository?.githubApp !== undefined),
286
+ },
287
+ }
288
+ : {};
188
289
 
189
290
  const requireAdmin: express.RequestHandler = async (req, res, next) => {
190
291
  if (!(await adminAccess.isAdmin(req.userEmail))) {
@@ -219,9 +320,10 @@ export function createSetupRoutes(
219
320
  }
220
321
  res.json({
221
322
  complete: kbReady(),
222
- awaitingRestart: awaitingRestart(settings, kb),
323
+ awaitingRestart: awaitingRestart(settings, kb, source),
223
324
  isAdmin: true,
224
325
  settings: settings.describe(),
326
+ ...repositoryStatus(),
225
327
  oidcVerification: await settings.oidcVerification(),
226
328
  ...(kbInit ? { kbInit } : bootFailure() !== null ? { kbInit: bootFailureKind() } : {}),
227
329
  ...(sync ? { sync: { url: sync.url, last: sync.lastSync() } } : {}),
@@ -251,7 +353,15 @@ export function createSetupRoutes(
251
353
  });
252
354
 
253
355
  async function handleSave(req: express.Request, res: express.Response): Promise<void> {
254
- const body = (req.body ?? {}) as { settings?: Record<string, unknown> };
356
+ const body = (req.body ?? {}) as {
357
+ settings?: Record<string, unknown>;
358
+ /** The admin's answer to the repository-change confirmation, when they have given one. */
359
+ confirmRepositoryChange?: unknown;
360
+ /** The count of open change requests that answer was given about. */
361
+ seenOpenChangeRequests?: unknown;
362
+ };
363
+ /** Lets the commit worker go again, once a move has held it. */
364
+ let releaseCommits: (() => void) | undefined;
255
365
  const entries: Record<string, string> = {};
256
366
  for (const [key, value] of Object.entries(body.settings ?? {})) {
257
367
  if (typeof value !== 'string') {
@@ -265,8 +375,40 @@ export function createSetupRoutes(
265
375
  // save that flips it false→true — whichever field arrives last — is the
266
376
  // one that must run the KB startup phase, regardless of which save
267
377
  // configured the branch model.
268
- const wasComplete = isComplete(settings, kb);
378
+ const wasComplete = isComplete(settings, kb, source);
379
+ // A different question, asked of the GATE: whether anyone is being
380
+ // served. Settings that are answered are not a deployment that works
381
+ // — one whose first clone failed has answered everything and serves
382
+ // nobody.
383
+ const wasServing = kbReady();
384
+ nameBranchesOfManagedRepository(entries);
269
385
  if (!(await connectionHoldsFor(entries, wasComplete, res))) return;
386
+ // AFTER the connection check, BEFORE anything is stored. After, because
387
+ // an address the host will not answer for is refused on its own terms
388
+ // and there is then nothing to confirm — nobody should have to agree to
389
+ // losing their working copies only to be told the token was missing.
390
+ // Before, because a refused save must destroy nothing: the check only
391
+ // reads the new address and dry-runs a push to it.
392
+ // Asked only of a deployment that has SERVED on the repository it would
393
+ // leave. A first run whose initialisation failed has answered
394
+ // everything and served nobody: correcting its address is finishing
395
+ // setup, and a question about leaving a repository nobody ever worked
396
+ // on would be a question with nothing behind it.
397
+ const hasServed = wasComplete && kbInit === null;
398
+ const repositoryChange = await repositoryChangeFor(
399
+ entries,
400
+ body.confirmRepositoryChange,
401
+ body.seenOpenChangeRequests,
402
+ hasServed,
403
+ res,
404
+ );
405
+ if (!repositoryChange) return;
406
+ // HELD FROM HERE, before anything is stored. The way chosen takes
407
+ // effect a few lines down, and with it the credential git is handed; a
408
+ // commit worker still running in between would push a queued commit
409
+ // from a working copy of the repository being left, with the
410
+ // credential of the one moved to. Let go in the `finally` below.
411
+ if (repositoryChange.changing) releaseCommits = await holdCommits();
270
412
  const oidc = await signInHoldsFor(entries, res);
271
413
  if (!oidc) return;
272
414
  // Recorded BEFORE the save: the record is keyed by the values it is
@@ -277,7 +419,75 @@ export function createSetupRoutes(
277
419
  if (oidc.record) {
278
420
  await settings.recordOidcVerification(oidc.record.state, oidc.record.credentials);
279
421
  }
280
- const { restartRequired, restartKeys } = await settings.save(entries, req.userId ?? null);
422
+ /**
423
+ * How many open change requests this save closed as "repository
424
+ * replaced". Nothing is ever deleted; `keep` leaves them exactly as they
425
+ * are, and answers 0.
426
+ *
427
+ * Closed BEFORE the address is stored, as a PRECONDITION of the save. A
428
+ * close that fails after the address is stored has nowhere to go: the
429
+ * address now matches, so the next save sees no change, asks nothing and
430
+ * never retries — leaving requests open on branches the new repository
431
+ * does not have, and their file locks refusing paths to everybody else,
432
+ * with no way for the admin to put it right. Refusing the save instead
433
+ * destroys nothing: the old address still stands, the working copies are
434
+ * untouched (the phase below never ran), and pressing Save again asks the
435
+ * same question and tries again.
436
+ *
437
+ * The other order of failure — closed, then the save itself fails — is
438
+ * recoverable by the admin, who wanted these requests closed and whose
439
+ * next Save completes the replacement. Closing deletes nothing either
440
+ * way.
441
+ */
442
+ let closedChangeRequests = 0;
443
+ if (repositoryChange.choice === 'close') {
444
+ try {
445
+ closedChangeRequests = await changeRequests.closeAsRepositoryReplaced();
446
+ } catch (err) {
447
+ log.error('the open change requests could not be closed, so the repository was not replaced:', { err });
448
+ res.status(500).json({
449
+ error:
450
+ 'The open change requests could not be closed, so the repository was not changed. ' +
451
+ 'Nothing was saved and no working copy was touched. Try again.',
452
+ });
453
+ return;
454
+ }
455
+ }
456
+ const saved = await settings.save(entries, req.userId ?? null);
457
+ /**
458
+ * THE WAY CHOSEN TAKES EFFECT ON THIS SAVE, in the two cases where the
459
+ * startup phase below runs for it.
460
+ *
461
+ * A deployment that was not serving has nothing on the mode it had: a
462
+ * first run, one whose initialisation failed, a boot that found its
463
+ * repository unreachable. Pinned there, the way out of a repository
464
+ * that does not work would be closed.
465
+ *
466
+ * And a deployment that WAS serving, when its admin has just confirmed
467
+ * the move. The mode in effect was once held until the next restart, so
468
+ * that the running process never presented one way's credential to
469
+ * another way's repository. That reason is answered differently now:
470
+ * the phase below sets the working copies of the repository that was
471
+ * left aside, under a held commit worker, before anything uses the new
472
+ * credential. And an admin of a hosted workspace has no restart to
473
+ * give, so a move that waited for one never happened.
474
+ *
475
+ * What is left pinned is a save that changes the mode WITHOUT moving
476
+ * the repository, which no path produces today; it keeps the restart.
477
+ */
478
+ if (source && (!wasServing || repositoryChange.changing)) source.takeEffect();
479
+ /**
480
+ * The restart the mode owes is read off the two modes themselves, not
481
+ * off what this save wrote: it is owed for as long as the mode chosen
482
+ * is not the one in effect, whichever save chose it, and no longer
483
+ * once a save chooses the mode in effect back.
484
+ */
485
+ const modePending = source !== undefined && source.chosen() !== source.mode();
486
+ const restartKeys = [
487
+ ...saved.restartKeys.filter((key) => key !== 'gitMode'),
488
+ ...(modePending ? ['gitMode'] : []),
489
+ ];
490
+ const restartRequired = restartKeys.length > 0;
281
491
  /** Whether this save put the stored folder names into the running process. */
282
492
  let layoutApplied = false;
283
493
  /** Whether this save put the stored branch model into the running process. */
@@ -316,11 +526,27 @@ export function createSetupRoutes(
316
526
  * Runs on the false→true completion transition — including when the
317
527
  * branch model was configured by an EARLIER save and the repository
318
528
  * URL arrives on a later one — and again on any save while a previous
319
- * setup-time run stands failed (`kbInit`), so a save is the retry. Never
320
- * on a re-save of a complete, healthy setup: with the gate open,
321
- * sessions may be live and that is no longer a quiet moment.
529
+ * setup-time run stands failed (`kbInit`), so a save is the retry.
530
+ *
531
+ * AND on a save that MOVES THE REPOSITORY, complete and healthy or
532
+ * not. That one is not a quiet moment — the gate is open and sessions
533
+ * may be live — but it is the moment the working copies stop belonging
534
+ * to the configured repository, and leaving them until the next restart
535
+ * is what made Save and Retry fail against a repository that was gone.
536
+ * The admin has just confirmed the move, so the phase runs, with the
537
+ * commit worker held for as long as it does, and the app opens on the
538
+ * new repository's content.
539
+ *
540
+ * Never on an ordinary re-save of a complete, healthy setup: with the
541
+ * gate open, sessions may be live and nothing needs doing.
322
542
  */
323
- if ((!wasComplete || kbInit !== null || bootFailure() !== null) && isComplete(settings, kb)) {
543
+ if (
544
+ (!wasComplete ||
545
+ kbInit !== null ||
546
+ bootFailure() !== null ||
547
+ repositoryChange.changing) &&
548
+ isComplete(settings, kb, source)
549
+ ) {
324
550
  /**
325
551
  * The folder names, applied BEFORE the phase for the same reason as
326
552
  * the branch model above: they are otherwise applied once at boot, so
@@ -343,6 +569,9 @@ export function createSetupRoutes(
343
569
  // One run at a time. The save chain already serializes handlers
344
570
  // whole, so no second run can start while one executes; the `??=`
345
571
  // is defense in depth should another invoker ever appear.
572
+ // A move runs with the commit worker held (see `holdCommits`
573
+ // above): nothing queued is written while working copies are set
574
+ // aside and cloned again.
346
575
  kbInitInFlight ??= kbStartupRunner.runAll();
347
576
  await kbInitInFlight;
348
577
  // Under KB_SAFE_BOOT a run that abandoned the phase still resolves
@@ -360,6 +589,7 @@ export function createSetupRoutes(
360
589
  const raw = initErr instanceof Error ? initErr.message : String(initErr);
361
590
  const msg = redactSecret(raw, [
362
591
  settings.resolve('gitToken'),
592
+ source?.credentials.token() ?? '',
363
593
  ...urlQuerySecrets(settings.resolve('kbRepoUrl')),
364
594
  ]);
365
595
  log.error(`KB initialization failed after setup completed: ${msg}`);
@@ -386,9 +616,15 @@ export function createSetupRoutes(
386
616
  )
387
617
  : restartRequired,
388
618
  complete: kbReady(),
389
- awaitingRestart: awaitingRestart(settings, kb),
619
+ awaitingRestart: awaitingRestart(settings, kb, source),
390
620
  settings: settings.describe(),
621
+ ...repositoryStatus(),
391
622
  oidcVerification: await settings.oidcVerification(),
623
+ // Only on the save that changed the repository, so an ordinary save
624
+ // carries no word about change requests at all.
625
+ ...(repositoryChange.changing
626
+ ? { repositoryChange: { choice: repositoryChange.choice, closedChangeRequests } }
627
+ : {}),
392
628
  });
393
629
  } catch (err) {
394
630
  if (err instanceof SettingsValidationError) {
@@ -399,9 +635,193 @@ export function createSetupRoutes(
399
635
  // the schema or the connection string.
400
636
  log.error('save failed:', { err });
401
637
  res.status(500).json({ error: 'Could not save these settings.' });
638
+ } finally {
639
+ releaseCommits?.();
402
640
  }
403
641
  }
404
642
 
643
+ /**
644
+ * Stop the commit worker until the function this resolves to is called.
645
+ * The hold the composition root offers runs a piece of work with the
646
+ * worker stopped; a move spans the store, the way taking effect and the
647
+ * startup phase, with refusals in between, so the hold is taken here and
648
+ * let go in the handler's `finally`. A mount without a worker holds
649
+ * nothing.
650
+ */
651
+ function holdCommits(): Promise<() => void> {
652
+ if (!changeRequests.whileCommitsHeld) return Promise.resolve(() => {});
653
+ return new Promise((held, failed) => {
654
+ changeRequests.whileCommitsHeld!<void>(() => new Promise<void>((release) => held(release))).catch(failed);
655
+ });
656
+ }
657
+
658
+ /**
659
+ * What this save does to the knowledge-base repository, and whether the
660
+ * admin has said yes to it.
661
+ *
662
+ * A MOVE IS ANY SAVE AFTER WHICH THE DEPLOYMENT READS ANOTHER REPOSITORY
663
+ * than the one it is running on, however it came about: another address,
664
+ * another way of having the repository (managed, GitHub, address and
665
+ * token), another repository on GitHub. So the question is asked of the
666
+ * one thing that knows where the repository is under every way — the
667
+ * repository source — comparing the address IN EFFECT with the one this
668
+ * save would choose. A mount without a source has the one way there
669
+ * always was, and the address is the setting.
670
+ *
671
+ * A move takes the working copies of the repository that is left out of
672
+ * use: one whose history the new repository holds is pointed at it and
673
+ * kept, any other is set aside and cloned fresh. Work that was never
674
+ * pushed leaves the app with a copy that is set aside. That is not
675
+ * something to discover afterwards, so the save is REFUSED (409) until the
676
+ * answer comes back with the admin's decision.
677
+ *
678
+ * Only a real move asks: an address that differs from the one in effect in
679
+ * nothing but its spelling — a trailing slash, a `.git` suffix, the case
680
+ * of the host — is the same repository (see `kb-fs/remote-url.ts`), and so
681
+ * is a first-run save, where no repository is in effect and nothing on
682
+ * disk could be left.
683
+ *
684
+ * Returns null when it has already answered the request.
685
+ */
686
+ async function repositoryChangeFor(
687
+ entries: Record<string, string>,
688
+ confirmation: unknown,
689
+ /** How many open change requests the screen showed when the answer was given, when it says. */
690
+ seenOpenChangeRequests: unknown,
691
+ /** Whether the deployment is serving on the repository in effect. One that never served and has no open request is not asked. */
692
+ hasServed: boolean,
693
+ res: express.Response,
694
+ ): Promise<{ changing: boolean; choice: RepositoryChangeChoice | null } | null> {
695
+ const unchanged = { changing: false, choice: null } as const;
696
+ const after = settings.resolveAfter(entries);
697
+ const now = (source ? source.url() : settings.resolve('kbRepoUrl')).trim();
698
+ const next = (source ? source.url(after) : after('kbRepoUrl')).trim();
699
+ if (now === '' || next === '' || sameRepository(now, next)) return unchanged;
700
+ // The count is what the choice is ABOUT, so it is read here rather than
701
+ // left for the screen to ask for separately: the two questions would
702
+ // otherwise be answered a round trip apart, and the number on screen
703
+ // would be the one from before whatever happened in between.
704
+ const openChangeRequests = await changeRequests.countOpen();
705
+ // An answer stands for the count it was given about. A request opened
706
+ // while the question stood was never offered "close", and the "keep" the
707
+ // screen sends when there is nothing to choose would decide it unasked:
708
+ // the question is put again, with the count as it is now.
709
+ const answered = confirmation === 'keep' || confirmation === 'close';
710
+ const answerIsCurrent = typeof seenOpenChangeRequests !== 'number' || seenOpenChangeRequests === openChangeRequests;
711
+ if (answered && answerIsCurrent) return { changing: true, choice: confirmation };
712
+ // A move all the same, and the phase treats it as one; nobody is asked.
713
+ // Open change requests are themselves proof that the deployment served,
714
+ // whatever `hasServed` says: a move that failed part-way leaves it gated
715
+ // with a failure standing, and the next save must still ask about them.
716
+ if (!answered && !hasServed && openChangeRequests === 0) return { changing: true, choice: null };
717
+ res.status(409).json({
718
+ error: 'This moves the deployment to another repository.',
719
+ repositoryChange: {
720
+ openChangeRequests,
721
+ // The way left and the way moved to, for the screen to name them.
722
+ // Equal when the move is within one way (another address, another
723
+ // repository on GitHub); absent from a mount with one way only.
724
+ ...(source ? { from: source.mode(), to: source.mode(after) } : {}),
725
+ },
726
+ });
727
+ return null;
728
+ }
729
+
730
+ /**
731
+ * A repository the deployment keeps for itself is new, and empty, and has
732
+ * no branches for anyone to look up: the branch model is ours to name. It
733
+ * is named only when nobody has — an admin's own answer, or one the
734
+ * environment supplies, stands. Every other mode has a repository that
735
+ * already exists, whose branches are asked for, never guessed.
736
+ */
737
+ function nameBranchesOfManagedRepository(entries: Record<string, string>): void {
738
+ if (!source) return;
739
+ const after = settings.resolveAfter(entries);
740
+ if (source.mode(after) !== 'managed') return;
741
+ if (after('defaultBranch') || after('protectedBranches')) return;
742
+ entries.defaultBranch = MANAGED_DEFAULT_BRANCH;
743
+ entries.protectedBranches = MANAGED_DEFAULT_BRANCH;
744
+ }
745
+
746
+ /**
747
+ * The same rule for a repository reached through a GitHub App: a saved
748
+ * connection is one GitHub has accepted for reading and writing, asked
749
+ * with the token git will present, which is the installation's.
750
+ *
751
+ * That token is also what keeps the repository's NAME honest. It is the
752
+ * one part of this connection an admin types, and a name the installation
753
+ * does not reach is refused here by GitHub, not by a list of ours.
754
+ *
755
+ * The repository has just said what it calls its trunk, so a deployment
756
+ * with no branch model is given that one: the same answer the setup
757
+ * screen fills in for a repository reached by its address.
758
+ */
759
+ async function githubConnectionHoldsFor(
760
+ setup: RepositorySetup,
761
+ entries: Record<string, string>,
762
+ after: (key: string) => string,
763
+ wasComplete: boolean,
764
+ res: express.Response,
765
+ ): Promise<boolean> {
766
+ const refuse = (problem: string, field = 'githubRepository') => {
767
+ res.status(400).json({ error: problem, problems: { [field]: problem } });
768
+ return false;
769
+ };
770
+ const app = setup.githubApp;
771
+ if (!app) return refuse('This deployment cannot connect to GitHub through a GitHub App.', 'gitMode');
772
+ if (!app.answered(after)) {
773
+ return refuse(
774
+ after('githubRepository')
775
+ ? 'Connect GitHub before choosing a repository.'
776
+ : 'Choose the repository the knowledge base lives in.',
777
+ );
778
+ }
779
+ const url = app.url(after);
780
+ const unanswered =
781
+ validateBranchModel({ defaultBranch: after('defaultBranch'), protectedBranches: after('protectedBranches') }) !== null;
782
+ // Against what is CHOSEN now, not what is in effect: a repository that
783
+ // was proven by the save that chose it is not proven again by the next.
784
+ const stored = (key: string) => settings.resolve(key);
785
+ const changes = setup.source.chosen() !== 'github-app' || url !== setup.source.url(stored);
786
+ if (!changes && (wasComplete || unanswered)) return true;
787
+ // BEFORE GitHub is asked anything with the installation token: that
788
+ // token reaches every repository the installation covers, and would
789
+ // answer for one the person connecting it could never have written to.
790
+ if (!app.permits(after('githubRepository'), after)) {
791
+ log.warn('a repository was named that the person who connected GitHub could not push to');
792
+ return refuse(
793
+ 'Choose a repository your own GitHub account can write to. If this one should be, press “Refresh the list” so it is brought up to date.',
794
+ );
795
+ }
796
+ try {
797
+ await app.prepare({ asked: true });
798
+ } catch (err) {
799
+ log.error('GitHub gave no token for the installation:', { detail: err instanceof Error ? err.message : String(err) });
800
+ }
801
+ const token = app.token();
802
+ if (!token) {
803
+ return refuse('GitHub gave this deployment no access. The app may have been uninstalled: connect it again.', 'gitMode');
804
+ }
805
+ const check = await checkConnection({ url, token, username: DEFAULT_GIT_USERNAME });
806
+ if (check.outcome !== 'connected') {
807
+ return refuse(
808
+ check.outcome === 'read-only'
809
+ ? 'The GitHub App can read that repository but not write to it. Grant it write access to the repository’s contents.'
810
+ : check.reason === 'unreachable'
811
+ ? 'GitHub could not be reached. Try again shortly.'
812
+ : 'The GitHub App cannot reach that repository. Add the repository to the app’s installation on GitHub.',
813
+ );
814
+ }
815
+ if (!after('defaultBranch') && !after('protectedBranches')) {
816
+ const trunk = check.defaultBranch || check.branches[0] || (check.empty ? MANAGED_DEFAULT_BRANCH : '');
817
+ if (trunk) {
818
+ entries.defaultBranch = trunk;
819
+ entries.protectedBranches = trunk;
820
+ }
821
+ }
822
+ return true;
823
+ }
824
+
405
825
  /**
406
826
  * A SAVED CONNECTION IS ONE THE HOST HAS ACCEPTED FOR READING AND WRITING.
407
827
  *
@@ -427,6 +847,25 @@ export function createSetupRoutes(
427
847
  res: express.Response,
428
848
  ): Promise<boolean> {
429
849
  const after = settings.resolveAfter(entries);
850
+ if (repository && repository.source.mode(after) === 'managed') {
851
+ // Nothing to prove to a host: the repository is the deployment's own.
852
+ // What has to hold is that it EXISTS before the save that points
853
+ // everything at it, so a disk that cannot take it refuses the choice
854
+ // instead of failing the first clone.
855
+ try {
856
+ await repository.ensureManaged(after('defaultBranch') || MANAGED_DEFAULT_BRANCH);
857
+ return true;
858
+ } catch (err) {
859
+ log.error('the managed repository could not be created:', { err });
860
+ const problem =
861
+ 'The repository could not be created on this deployment’s storage. Check that its backups folder is writable, or connect a repository of your own.';
862
+ res.status(400).json({ error: problem, problems: { gitMode: problem } });
863
+ return false;
864
+ }
865
+ }
866
+ if (repository && repository.source.mode(after) === 'github-app') {
867
+ return githubConnectionHoldsFor(repository, entries, after, wasComplete, res);
868
+ }
430
869
  const now: RepositoryConnection = {
431
870
  url: settings.resolve('kbRepoUrl'),
432
871
  token: settings.resolve('gitToken'),
@@ -451,7 +890,10 @@ export function createSetupRoutes(
451
890
  // the environment, whose field the form cannot even edit) is that token's
452
891
  // first and only pairing, not a change of it.
453
892
  const tokenSupplied = Boolean(entries.gitToken?.trim());
454
- if (now.url && next.url !== now.url && next.token && !tokenSupplied) {
893
+ // A different SPELLING of the configured address is the same repository,
894
+ // so the stored token is still the token it was saved for: asking for it
895
+ // again over a trailing slash would be a question with no answer.
896
+ if (now.url && !sameRepository(next.url, now.url) && next.token && !tokenSupplied) {
455
897
  return settings.sourceOf('gitToken') === 'env'
456
898
  ? refuse({
457
899
  kbRepoUrl:
@@ -763,8 +1205,12 @@ export function createSetupRoutes(
763
1205
  * deployment signs in perfectly well without it, and gating on it would lock
764
1206
  * an admin out of the screen where they would set it up.
765
1207
  */
766
- export function settingsAnswered(settings: DeploymentSettingsService): boolean {
767
- const kb = Boolean(settings.resolve('kbRepoUrl') && settings.resolve('gitToken'));
1208
+ export function settingsAnswered(
1209
+ settings: DeploymentSettingsService,
1210
+ /** What answers for the repository. Absent: an address and a token, the one way there was. */
1211
+ source?: Pick<RepositorySource, 'answered'>,
1212
+ ): boolean {
1213
+ const kb = source ? source.answered() : Boolean(settings.resolve('kbRepoUrl') && settings.resolve('gitToken'));
768
1214
  const branches =
769
1215
  validateBranchModel({
770
1216
  defaultBranch: settings.resolve('defaultBranch'),
@@ -791,14 +1237,16 @@ export function settingsAnswered(settings: DeploymentSettingsService): boolean {
791
1237
  export function isComplete(
792
1238
  settings: DeploymentSettingsService,
793
1239
  kb: Pick<KbContext, 'isBranchModelConfigured'>,
1240
+ source?: Pick<RepositorySource, 'answered'>,
794
1241
  ): boolean {
795
- return settingsAnswered(settings) && kb.isBranchModelConfigured();
1242
+ return settingsAnswered(settings, source) && kb.isBranchModelConfigured();
796
1243
  }
797
1244
 
798
1245
  /** Answered, but not yet in effect: everything is stored, the process is stale. */
799
1246
  export function awaitingRestart(
800
1247
  settings: DeploymentSettingsService,
801
1248
  kb: Pick<KbContext, 'isBranchModelConfigured'>,
1249
+ source?: Pick<RepositorySource, 'answered'>,
802
1250
  ): boolean {
803
- return settingsAnswered(settings) && !kb.isBranchModelConfigured();
1251
+ return settingsAnswered(settings, source) && !kb.isBranchModelConfigured();
804
1252
  }
@@ -12,6 +12,7 @@ import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
12
12
  import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
13
13
  import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
14
14
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
15
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
15
16
  import { SpillStore } from '../../workspace/spill-store.js';
16
17
  import { DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
17
18
  import type { IAccessControl } from '../../access/access-control.interface.js';
@@ -111,7 +112,7 @@ async function start(): Promise<string> {
111
112
  registerWorkspaceTools(
112
113
  registry, router, fakeAuth, toolHandler,
113
114
  new SpillStore(join(tmpdir(), 'bevel-test-spills')), new DocExtractService(join(tmpdir(), 'bevel-test-doc-extract')), allowAll, testKbContext({ kbDirName: KB_DIR }),
114
- { service: {} as never, enabled: false, kb: testKbContext({ kbDirName: KB_DIR }), recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks() },
115
+ { recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
115
116
  new RoutineWritePolicyService(),
116
117
  {} as never,
117
118
  checker,