@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
@@ -0,0 +1,326 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { testKbContext } from '../../../__tests__/kb-context.js';
3
+ import { NodeFs } from '../../kb-fs/node-fs.js';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { WorkspaceService } from '../workspace.service.js';
10
+ import { NodeGitRunner } from '../../workflow/git/node-git-runner.js';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ /**
15
+ * A working copy left on disk by a deployment that has since been pointed at
16
+ * a DIFFERENT repository.
17
+ *
18
+ * The workspace service adopts whatever clone it finds on disk, and a clone
19
+ * fetches through the address stored in its own `remote.origin.url` — so an
20
+ * adopted one of the repository that was replaced answers "repository not
21
+ * found" on every fetch from then on, with nothing to re-clone it. It is set
22
+ * aside and cloned fresh instead.
23
+ *
24
+ * SET ASIDE, never deleted — the same rule, and the same destination, as the
25
+ * KB startup phase's sweep. This path has reached no remote of its own: it
26
+ * runs wherever the address is stored but unproven (a gated boot, a
27
+ * break-glass start, a save that changed the address before the phase got to
28
+ * it), so a mistyped host name must not cost anyone commits that exist
29
+ * nowhere else.
30
+ */
31
+
32
+ const BRANCH = 'main';
33
+
34
+ async function git(cwd: string, args: string[]): Promise<string> {
35
+ const { stdout } = await execFileAsync('git', args, {
36
+ cwd,
37
+ env: {
38
+ ...process.env,
39
+ GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
40
+ GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
41
+ },
42
+ });
43
+ return stdout.toString();
44
+ }
45
+
46
+ let root: string;
47
+ let workspacesRoot: string;
48
+
49
+ beforeEach(async () => {
50
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'ws-replaced-'));
51
+ workspacesRoot = path.join(root, 'workspaces');
52
+ await fs.mkdir(workspacesRoot, { recursive: true });
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await fs.rm(root, { recursive: true, force: true });
57
+ });
58
+
59
+ /** A bare repository with one commit on `main`, whose file says `marker`. */
60
+ async function upstream(name: string, marker: string): Promise<string> {
61
+ const bare = path.join(root, `${name}.git`);
62
+ await git(root, ['init', '--bare', '-b', BRANCH, bare]);
63
+ const seed = path.join(root, `.seed-${name}`);
64
+ await fs.mkdir(seed);
65
+ await git(seed, ['init', '-b', BRANCH]);
66
+ await fs.writeFile(path.join(seed, 'marker.txt'), marker, 'utf8');
67
+ await git(seed, ['add', '-A']);
68
+ await git(seed, ['commit', '-m', 'init']);
69
+ await git(seed, ['remote', 'add', 'origin', bare]);
70
+ await git(seed, ['push', 'origin', BRANCH]);
71
+ return bare;
72
+ }
73
+
74
+ function service(
75
+ kbRepoUrl: () => string,
76
+ setAsideRoot?: string,
77
+ aroundSetAside?: (workspaceId: string, move: () => Promise<void>) => Promise<void>,
78
+ ) {
79
+ return new WorkspaceService(
80
+ workspacesRoot,
81
+ kbRepoUrl,
82
+ testKbContext({ branchModel: { defaultBranch: BRANCH, protectedBranches: [BRANCH] } }),
83
+ new NodeFs(),
84
+ new NodeGitRunner(),
85
+ setAsideRoot,
86
+ aroundSetAside,
87
+ );
88
+ }
89
+
90
+ const cloneDir = () => path.join(workspacesRoot, encodeURIComponent(BRANCH), 'knowledge-base');
91
+
92
+ /** Every working copy under a set-aside root, as repo directories. */
93
+ async function setAside(setAsideRoot = path.join(root, 'replaced-working-copies')): Promise<string[]> {
94
+ const stamps = await fs.readdir(setAsideRoot).catch(() => [] as string[]);
95
+ const kept: string[] = [];
96
+ for (const stamp of stamps) {
97
+ for (const id of await fs.readdir(path.join(setAsideRoot, stamp))) {
98
+ kept.push(path.join(setAsideRoot, stamp, id));
99
+ }
100
+ }
101
+ return kept;
102
+ }
103
+
104
+ describe('a working copy of a repository that was replaced', () => {
105
+ it('is set aside and cloned fresh from the configured address', async () => {
106
+ const old = await upstream('old', 'old repository');
107
+ const replacement = await upstream('replacement', 'new repository');
108
+ // A clone of the OLD repository, sitting on disk from before the change.
109
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
110
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
111
+
112
+ await service(() => replacement).getOrCreateForBranch(BRANCH);
113
+
114
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(replacement);
115
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
116
+ });
117
+
118
+ it('keeps the work that was only ever committed there, in the set-aside folder', async () => {
119
+ const old = await upstream('old', 'old repository');
120
+ const replacement = await upstream('replacement', 'new repository');
121
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
122
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
123
+ // Committed here and pushed nowhere. The address may yet turn out to be a
124
+ // typo, so this is the last copy of it in the world.
125
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
126
+ await git(cloneDir(), ['add', '-A']);
127
+ await git(cloneDir(), ['commit', '-m', 'local work']);
128
+ const head = (await git(cloneDir(), ['rev-parse', 'HEAD'])).trim();
129
+
130
+ await service(() => replacement).getOrCreateForBranch(BRANCH);
131
+
132
+ const kept = await setAside();
133
+ expect(kept).toHaveLength(1);
134
+ // Still a repository, still holding the commit, still reachable by hand:
135
+ // `git log` there shows it and `git push <address> <branch>` sends it on.
136
+ expect((await git(kept[0]!, ['rev-parse', 'HEAD'])).trim()).toBe(head);
137
+ expect(await fs.readFile(path.join(kept[0]!, 'unpushed.md'), 'utf8')).toBe('local work');
138
+ // And the fresh clone really is of the new repository.
139
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
140
+ });
141
+
142
+ it('leaves a clone of the configured repository exactly where it is', async () => {
143
+ const configured = await upstream('configured', 'configured');
144
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
145
+ await git(root, ['clone', '-b', BRANCH, configured, cloneDir()]);
146
+ // Committed here and never pushed: a needless re-clone would lose it.
147
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
148
+ await git(cloneDir(), ['add', '-A']);
149
+ await git(cloneDir(), ['commit', '-m', 'local work']);
150
+ const head = (await git(cloneDir(), ['rev-parse', 'HEAD'])).trim();
151
+
152
+ await service(() => configured).getOrCreateForBranch(BRANCH);
153
+
154
+ expect((await git(cloneDir(), ['rev-parse', 'HEAD'])).trim()).toBe(head);
155
+ expect(await setAside()).toEqual([]);
156
+ });
157
+
158
+ it('keeps a clone whose address differs only in spelling', async () => {
159
+ const configured = await upstream('configured', 'configured');
160
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
161
+ await git(root, ['clone', '-b', BRANCH, configured, cloneDir()]);
162
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
163
+ await git(cloneDir(), ['add', '-A']);
164
+ await git(cloneDir(), ['commit', '-m', 'local work']);
165
+ const head = (await git(cloneDir(), ['rev-parse', 'HEAD'])).trim();
166
+
167
+ // The same path with a trailing slash — the same repository.
168
+ await service(() => `${configured}/`).getOrCreateForBranch(BRANCH);
169
+
170
+ expect((await git(cloneDir(), ['rev-parse', 'HEAD'])).trim()).toBe(head);
171
+ expect(await setAside()).toEqual([]);
172
+ });
173
+
174
+ it('keeps a clone when the process does not know where the repository is', async () => {
175
+ const configured = await upstream('configured', 'configured');
176
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
177
+ await git(root, ['clone', '-b', BRANCH, configured, cloneDir()]);
178
+
179
+ // Nothing configured yet: "could not tell" is no reason to move work.
180
+ await service(() => '').getOrCreateForBranch(BRANCH);
181
+
182
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(configured);
183
+ expect(await setAside()).toEqual([]);
184
+ });
185
+ });
186
+
187
+ /**
188
+ * WHERE it is set aside is not a detail. A deployment runs from an image whose
189
+ * filesystem is replaced on every recreate, and only the mounted volumes
190
+ * survive that — so a working copy kept beside the workspaces root was kept
191
+ * until the next `docker compose up` and no longer, while the screen had
192
+ * promised the admin it was there to recover from. The composition root roots
193
+ * this service and the KB startup phase at ONE directory under the backups
194
+ * volume; this is the half of that the service owes.
195
+ */
196
+ describe('where a working copy is set aside', () => {
197
+ it('goes to the root it was given, not beside the workspaces root', async () => {
198
+ const old = await upstream('old', 'old repository');
199
+ const replacement = await upstream('replacement', 'new repository');
200
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
201
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
202
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
203
+ await git(cloneDir(), ['add', '-A']);
204
+ await git(cloneDir(), ['commit', '-m', 'local work']);
205
+ // Somewhere the workspaces root cannot be reached from by walking up:
206
+ // the backups volume, as the deployment mounts it.
207
+ const backups = path.join(root, 'backups', 'replaced-working-copies');
208
+
209
+ await service(() => replacement, backups).getOrCreateForBranch(BRANCH);
210
+
211
+ const kept = await setAside(backups);
212
+ expect(kept).toHaveLength(1);
213
+ expect(await fs.readFile(path.join(kept[0]!, 'unpushed.md'), 'utf8')).toBe('local work');
214
+ // And nothing was left in the place that does not outlive the container.
215
+ expect(await setAside()).toEqual([]);
216
+ });
217
+
218
+ it('falls back beside the workspaces root when no root is given', async () => {
219
+ const old = await upstream('old', 'old repository');
220
+ const replacement = await upstream('replacement', 'new repository');
221
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
222
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
223
+
224
+ await service(() => replacement).getOrCreateForBranch(BRANCH);
225
+
226
+ expect(await setAside()).toHaveLength(1);
227
+ });
228
+ });
229
+
230
+ /**
231
+ * A branch opened onto a clone of another repository sets it aside on the
232
+ * spot, with nobody having stopped anything for it. What the deployment does
233
+ * around the startup phase's set-asides (the commit worker held, the copy's
234
+ * queued commits held back, its locks dropped) it is handed here to do around
235
+ * this one.
236
+ */
237
+ describe('what surrounds the setting aside of one working copy', () => {
238
+ async function onTheOldRepository() {
239
+ const old = await upstream('old', 'old repository');
240
+ const replacement = await upstream('replacement', 'new repository');
241
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
242
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
243
+ return { old, replacement };
244
+ }
245
+
246
+ it('is given the working copy and the move, and the copy is where it was until the move is run', async () => {
247
+ const { old, replacement } = await onTheOldRepository();
248
+ const seen: string[] = [];
249
+ const svc = service(() => replacement, undefined, async (workspaceId, move) => {
250
+ seen.push(`before ${workspaceId}: ${(await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()}`);
251
+ await move();
252
+ seen.push(`after: ${await fs.access(cloneDir()).then(() => 'still there', () => 'gone')}`);
253
+ });
254
+
255
+ await svc.getOrCreateForBranch(BRANCH);
256
+
257
+ expect(seen).toEqual([`before ${encodeURIComponent(BRANCH)}: ${old}`, 'after: gone']);
258
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
259
+ });
260
+
261
+ it('refuses the branch, and leaves the copy where it was, when what must happen first fails', async () => {
262
+ const { old, replacement } = await onTheOldRepository();
263
+ const svc = service(() => replacement, undefined, async () => {
264
+ throw new Error('the queue could not be held back');
265
+ });
266
+
267
+ await expect(svc.getOrCreateForBranch(BRANCH)).rejects.toThrow('the queue could not be held back');
268
+
269
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(old);
270
+ expect(await setAside()).toEqual([]);
271
+ });
272
+
273
+ /**
274
+ * What surrounds the move can wait: for the commit in flight, or for the
275
+ * startup phase, which may set this very copy aside in the meantime.
276
+ */
277
+ it('moves nothing, and still opens the branch, when the startup phase took the copy while it waited', async () => {
278
+ const { replacement } = await onTheOldRepository();
279
+ const phaseKept = path.join(root, 'kept-by-the-phase');
280
+ const svc: WorkspaceService = service(() => replacement, undefined, async (workspaceId, move) => {
281
+ // The phase, while this call waited: the copy moved out, and said so.
282
+ await fs.rename(cloneDir(), phaseKept);
283
+ svc.forgetClone(workspaceId);
284
+ await move();
285
+ });
286
+
287
+ await svc.getOrCreateForBranch(BRANCH);
288
+
289
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
290
+ // Nothing of this call's own was set aside: the phase's copy is the only one.
291
+ expect(await setAside()).toEqual([]);
292
+ expect(await fs.readFile(path.join(phaseKept, 'marker.txt'), 'utf8')).toBe('old repository');
293
+ });
294
+
295
+ it('moves it once for two callers opening the branch together, and serves both', async () => {
296
+ const { replacement } = await onTheOldRepository();
297
+ let moves = 0;
298
+ const svc = service(() => replacement, undefined, async (_workspaceId, move) => {
299
+ moves += 1;
300
+ await move();
301
+ });
302
+
303
+ const [first, second] = await Promise.all([svc.getOrCreateForBranch(BRANCH), svc.getOrCreateForBranch(BRANCH)]);
304
+
305
+ expect(moves).toBe(1);
306
+ expect(first.repoDir).toBe(second.repoDir);
307
+ expect(await setAside()).toHaveLength(1);
308
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
309
+ });
310
+ });
311
+
312
+ describe('forgetClone', () => {
313
+ it('drops the cached directory, so the next open clones again', async () => {
314
+ const configured = await upstream('configured', 'configured');
315
+ const svc = service(() => configured);
316
+ await svc.getOrCreateForBranch(BRANCH);
317
+ // What the KB startup phase does to a clone of another repository: moves
318
+ // it out of the workspaces root and says so.
319
+ await fs.rm(cloneDir(), { recursive: true, force: true });
320
+ svc.forgetClone(encodeURIComponent(BRANCH));
321
+
322
+ await svc.getOrCreateForBranch(BRANCH);
323
+
324
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('configured');
325
+ });
326
+ });
@@ -1122,7 +1122,7 @@ describe('WorkspaceService.readAllKbFiles', () => {
1122
1122
  });
1123
1123
  });
1124
1124
 
1125
- describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1125
+ describe('WorkspaceService.unzipFile — per-entry write guard', () => {
1126
1126
  let root: string;
1127
1127
  let svc: WorkspaceService;
1128
1128
  let workspaceDir: string;
@@ -1154,12 +1154,12 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1154
1154
  it('skips entries the write guard rejects and never writes them to disk', async () => {
1155
1155
  await writeZip('a.zip', { 'keep.md': 'ok', 'blocked.md': 'no' });
1156
1156
  const res = await svc.unzipFile(workspaceId, 'a.zip', 'out', async (wsPath) => {
1157
- if (wsPath.endsWith('blocked.md')) throw new Error('Blocked by the ontology-session boundary');
1157
+ if (wsPath.endsWith('blocked.md')) throw new Error('Not in this conversation.');
1158
1158
  });
1159
1159
  // An unprefixed destination is the repository's own `out/`, and the
1160
1160
  // extracted paths are reported workspace-relative, so prefixed.
1161
1161
  expect(res.extracted).toEqual(['knowledge-base/out/keep.md']);
1162
- expect(res.skipped).toContainEqual({ path: 'blocked.md', reason: 'Blocked by the ontology-session boundary' });
1162
+ expect(res.skipped).toContainEqual({ path: 'blocked.md', reason: 'Not in this conversation.' });
1163
1163
  expect((await fs.readFile(path.join(repoDir, 'out', 'keep.md'))).toString()).toBe('ok');
1164
1164
  await expect(fs.readFile(path.join(repoDir, 'out', 'blocked.md'))).rejects.toThrow();
1165
1165
  await expect(fs.stat(path.join(workspaceDir, 'out'))).rejects.toMatchObject({ code: 'ENOENT' });
@@ -1173,18 +1173,18 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1173
1173
  await fs.writeFile(path.join(repoDir, 'c.zip'), zip.toBuffer());
1174
1174
 
1175
1175
  const res = await svc.unzipFile(workspaceId, 'c.zip', 'out', async (wsPath) => {
1176
- if (wsPath.includes('blocked-dir')) throw new Error('Blocked by the ontology-session boundary');
1176
+ if (wsPath.includes('blocked-dir')) throw new Error('Not in this conversation.');
1177
1177
  });
1178
1178
 
1179
1179
  expect(res.extracted).toEqual(['knowledge-base/out/keep.md']);
1180
- expect(res.skipped).toContainEqual({ path: 'blocked-dir/', reason: 'Blocked by the ontology-session boundary' });
1180
+ expect(res.skipped).toContainEqual({ path: 'blocked-dir/', reason: 'Not in this conversation.' });
1181
1181
  await expect(fs.stat(path.join(repoDir, 'out', 'blocked-dir'))).rejects.toThrow();
1182
1182
  });
1183
1183
 
1184
1184
  it('does not create the destination directory when every entry is blocked', async () => {
1185
1185
  await writeZip('d.zip', { 'a.md': '1', 'b.md': '2' });
1186
1186
  const res = await svc.unzipFile(workspaceId, 'd.zip', 'out', async () => {
1187
- throw new Error('Blocked by the ontology-session boundary');
1187
+ throw new Error('Not in this conversation.');
1188
1188
  });
1189
1189
  expect(res.extracted).toEqual([]);
1190
1190
  expect(res.skipped).toHaveLength(2);
@@ -1213,11 +1213,22 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1213
1213
  'There is no file or directory at "knowledge-base/nope.zip" in this workspace. Check the path with list_files.',
1214
1214
  });
1215
1215
  // A name carrying a line break cannot forge a second line of the answer,
1216
- // in the message or in the `path` a JSON consumer reads.
1217
- const forged = 'a\nb\u2028c.zip';
1216
+ // in the message or in the `path` a JSON consumer reads. A LINE BREAK no
1217
+ // longer reaches this refusal at all — the normaliser now refuses a
1218
+ // control character outright, because git reads one in a path as the end
1219
+ // of an object name (see `repo-path.ts`) — so the forged name here
1220
+ // carries U+2028, which is a line break to a renderer and nothing to git:
1221
+ // still accepted as a file name, still escaped on the way out.
1222
+ const forged = 'a\u2028c.zip';
1218
1223
  await expect(svc.unzipFile(workspaceId, forged)).rejects.toMatchObject({
1219
1224
  status: 404,
1220
- payload: { kind: 'not_found', path: 'knowledge-base/a\\nb\\u2028c.zip' },
1225
+ payload: { kind: 'not_found', path: 'knowledge-base/a\\u2028c.zip' },
1226
+ });
1227
+ // And the same invariant at the gate that now owns the line break: the
1228
+ // refusal names the path with the break escaped, never raw.
1229
+ await expect(svc.unzipFile(workspaceId, 'a\nb.zip')).rejects.toMatchObject({
1230
+ status: 400,
1231
+ payload: { kind: 'path-control-character', path: 'a\\nb.zip' },
1221
1232
  });
1222
1233
  });
1223
1234
 
@@ -77,6 +77,49 @@ describe('WorkspaceService — a branch origin does not have', () => {
77
77
  });
78
78
  });
79
79
 
80
+ /**
81
+ * The second defect on this ticket. A bootstrap that fails to clone used to
82
+ * leave its workspace directory behind, and a directory named after a branch
83
+ * is one of the ways the platform remembers having KNOWN a branch. So the
84
+ * first attempt at a typo answered 404 and then quietly created the evidence
85
+ * that turned every later attempt into 410 — the reader was told a branch
86
+ * they had invented "no longer exists on the remote".
87
+ */
88
+ it('leaves nothing on disk when the clone fails, so a second attempt is still a 404', async () => {
89
+ const svc = service();
90
+
91
+ await expect(svc.getOrCreateForBranch('nobody/never-made-this')).rejects.toMatchObject({
92
+ name: 'BranchNotFoundError',
93
+ status: 404,
94
+ });
95
+ // The failed bootstrap rolled back completely — not the clone directory,
96
+ // not the workspace shell around it.
97
+ expect(await fs.readdir(workspacesRoot)).toEqual([]);
98
+
99
+ // The same name again, on the same instance that has now already failed on
100
+ // it once. Nothing about the first failure may change this answer.
101
+ await expect(svc.getOrCreateForBranch('nobody/never-made-this')).rejects.toMatchObject({
102
+ name: 'BranchNotFoundError',
103
+ status: 404,
104
+ message: 'There is no branch named nobody/never-made-this.',
105
+ payload: { kind: 'branch-not-found', branch: 'nobody/never-made-this' },
106
+ });
107
+ expect(await fs.readdir(workspacesRoot)).toEqual([]);
108
+ });
109
+
110
+ it('does not count a bare directory with no clone in it as a branch it knew', async () => {
111
+ // A shell left by an older process (one that crashed mid-bootstrap, or that
112
+ // predates the rollback above). The "have I heard of this branch" probe asks
113
+ // for a COMPLETED clone, so a directory with no `.git` under it is absence:
114
+ // the honest 404 survives whatever is lying around the workspaces root.
115
+ await fs.mkdir(path.join(workspacesRoot, workspaceIdForBranch('nobody/never-made-this')), { recursive: true });
116
+
117
+ await expect(service().getOrCreateForBranch('nobody/never-made-this')).rejects.toMatchObject({
118
+ name: 'BranchNotFoundError',
119
+ status: 404,
120
+ });
121
+ });
122
+
80
123
  it('a name a listing showed us, and origin no longer has, stays a 410', async () => {
81
124
  const svc = service();
82
125
  // What the branch selector saw while the draft still existed.
@@ -12,6 +12,7 @@ import { DocExtractService } from '../file-readers/doc-extract.service.js';
12
12
  import { RoutineWritePolicyService } from '../routine-write-policy.js';
13
13
  import { SpillStore } from '../spill-store.js';
14
14
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
15
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
15
16
  import { registerWorkspaceTools } from '../workspace.tools.js';
16
17
 
17
18
  /**
@@ -42,11 +43,9 @@ async function descriptions(registry: ToolRegistry = new ToolRegistry()): Promis
42
43
  {} as unknown as IAccessControl,
43
44
  kb,
44
45
  {
45
- service: {} as never,
46
- enabled: false,
47
- kb,
48
46
  recoveryBotEmail: 'recovery-bot@bevel.local',
49
47
  hooks: new WorkflowHooks(),
48
+ notes: new ToolDescriptionNotes(),
50
49
  },
51
50
  new RoutineWritePolicyService(),
52
51
  {} as never,