@bevel-software/platform-core-backend 0.22.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 (325) 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 +30 -11
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +9 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +112 -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.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +13 -15
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/auth/account-admission.d.ts +58 -7
  34. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  35. package/dist/modules/auth/account-admission.js +44 -1
  36. package/dist/modules/auth/account-admission.js.map +1 -1
  37. package/dist/modules/auth/account.routes.d.ts +1 -1
  38. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  39. package/dist/modules/auth/account.routes.js +61 -5
  40. package/dist/modules/auth/account.routes.js.map +1 -1
  41. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  42. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.middleware.js +18 -7
  44. package/dist/modules/auth/auth.middleware.js.map +1 -1
  45. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  46. package/dist/modules/auth/auth.routes.js +8 -0
  47. package/dist/modules/auth/auth.routes.js.map +1 -1
  48. package/dist/modules/auth/auth.service.d.ts +70 -2
  49. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  50. package/dist/modules/auth/auth.service.js +176 -7
  51. package/dist/modules/auth/auth.service.js.map +1 -1
  52. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  53. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  54. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  55. package/dist/modules/database/core-schema.d.ts +41 -24
  56. package/dist/modules/database/core-schema.d.ts.map +1 -1
  57. package/dist/modules/database/core-schema.js +35 -27
  58. package/dist/modules/database/core-schema.js.map +1 -1
  59. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  60. package/dist/modules/kb-fs/branch-name.js +12 -2
  61. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  62. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  63. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  64. package/dist/modules/kb-fs/repo-path.js +79 -0
  65. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  66. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  67. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  68. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  69. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  70. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  71. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  72. package/dist/modules/kb-sync/sync-auth.js +1 -1
  73. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  74. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  75. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  76. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  77. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  78. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  79. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  80. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  81. package/dist/modules/settings/deployment-settings.service.js +8 -3
  82. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  83. package/dist/modules/settings/setup.routes.d.ts +52 -1
  84. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  85. package/dist/modules/settings/setup.routes.js +207 -19
  86. package/dist/modules/settings/setup.routes.js.map +1 -1
  87. package/dist/modules/skills/skills.contract.d.ts +90 -18
  88. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  89. package/dist/modules/skills/skills.contract.js +4 -1
  90. package/dist/modules/skills/skills.contract.js.map +1 -1
  91. package/dist/modules/skills/skills.service.d.ts +98 -9
  92. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  93. package/dist/modules/skills/skills.service.js +239 -37
  94. package/dist/modules/skills/skills.service.js.map +1 -1
  95. package/dist/modules/skills/skills.tools.d.ts +7 -0
  96. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  97. package/dist/modules/skills/skills.tools.js +71 -7
  98. package/dist/modules/skills/skills.tools.js.map +1 -1
  99. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  100. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  101. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  102. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  103. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  104. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  105. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  106. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  107. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  108. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  109. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  110. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  111. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  112. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  113. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  114. package/dist/modules/tool-helpers/validate-token.js +1 -1
  115. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  116. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  117. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  118. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  119. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  120. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  121. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  122. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  123. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  124. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  125. package/dist/modules/workflow/file-lock.service.js +30 -0
  126. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  127. package/dist/modules/workflow/git/git.service.d.ts +69 -0
  128. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  129. package/dist/modules/workflow/git/git.service.js +153 -2
  130. package/dist/modules/workflow/git/git.service.js.map +1 -1
  131. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  132. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  133. package/dist/modules/workflow/pending-commits.service.js +55 -0
  134. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  135. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  136. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  137. package/dist/modules/workflow/workflow-hooks.js +16 -1
  138. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  139. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  140. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  141. package/dist/modules/workflow/workflow.service.js +111 -4
  142. package/dist/modules/workflow/workflow.service.js.map +1 -1
  143. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  144. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  145. package/dist/modules/workspace/agent-access.gate.js +123 -0
  146. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  147. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  148. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  149. package/dist/modules/workspace/routine-write-policy.js +5 -6
  150. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  151. package/dist/modules/workspace/session-sink.d.ts +5 -5
  152. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  153. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  154. package/dist/modules/workspace/set-aside-clone.js +92 -0
  155. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  156. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  157. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  158. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  159. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  160. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  161. package/dist/modules/workspace/workspace.routes.js +92 -3
  162. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  163. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  164. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  165. package/dist/modules/workspace/workspace.service.js +255 -25
  166. package/dist/modules/workspace/workspace.service.js.map +1 -1
  167. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  168. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  169. package/dist/modules/workspace/workspace.tools.js +309 -115
  170. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  171. package/dist/modules/write-access/write-access.d.ts +60 -0
  172. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  173. package/dist/modules/write-access/write-access.js +129 -0
  174. package/dist/modules/write-access/write-access.js.map +1 -0
  175. package/dist/shared/domain-errors.d.ts +25 -0
  176. package/dist/shared/domain-errors.d.ts.map +1 -1
  177. package/dist/shared/domain-errors.js +28 -0
  178. package/dist/shared/domain-errors.js.map +1 -1
  179. package/dist/shared/git.contract.d.ts +20 -0
  180. package/dist/shared/git.contract.d.ts.map +1 -1
  181. package/dist/shared/git.contract.js +26 -0
  182. package/dist/shared/git.contract.js.map +1 -1
  183. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  184. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  185. package/dist/tenancy/static-tenant-source.js +0 -2
  186. package/dist/tenancy/static-tenant-source.js.map +1 -1
  187. package/migrations/0014_change_request_closed_reason.sql +1 -0
  188. package/migrations/0015_account_deactivation.sql +3 -0
  189. package/migrations/meta/0014_snapshot.json +2265 -0
  190. package/migrations/meta/0015_snapshot.json +2271 -0
  191. package/migrations/meta/_journal.json +14 -0
  192. package/package.json +3 -3
  193. package/src/__tests__/kb-layout-config.test.ts +0 -2
  194. package/src/__tests__/retired-settings.test.ts +96 -0
  195. package/src/core/__tests__/gated-boot.test.ts +132 -0
  196. package/src/core/__tests__/lifecycle.test.ts +170 -1
  197. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  198. package/src/core/core-ports.ts +11 -1
  199. package/src/core/create-core-server.ts +32 -14
  200. package/src/core/create-core-services.ts +129 -23
  201. package/src/core/lifecycle.ts +98 -13
  202. package/src/core-config.ts +13 -14
  203. package/src/index.ts +12 -1
  204. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  205. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  206. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  207. package/src/modules/access/access-control.interface.ts +43 -12
  208. package/src/modules/access/access-control.service.ts +234 -36
  209. package/src/modules/access/access.routes.ts +13 -15
  210. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  211. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  212. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  213. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  214. package/src/modules/auth/__tests__/auth.service.test.ts +1 -1
  215. package/src/modules/auth/account-admission.ts +78 -9
  216. package/src/modules/auth/account.routes.ts +63 -7
  217. package/src/modules/auth/auth.middleware.ts +19 -8
  218. package/src/modules/auth/auth.routes.ts +8 -0
  219. package/src/modules/auth/auth.service.ts +179 -6
  220. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  221. package/src/modules/database/core-schema.ts +35 -27
  222. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  223. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  224. package/src/modules/kb-fs/branch-name.ts +14 -1
  225. package/src/modules/kb-fs/repo-path.ts +85 -0
  226. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  227. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  228. package/src/modules/kb-sync/sync-auth.ts +2 -2
  229. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  230. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  231. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  232. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  233. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  234. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  235. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  236. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  237. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  238. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  239. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  240. package/src/modules/settings/deployment-settings.service.ts +8 -3
  241. package/src/modules/settings/setup.routes.ts +263 -19
  242. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  243. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  244. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  245. package/src/modules/skills/skills.contract.ts +91 -18
  246. package/src/modules/skills/skills.service.ts +278 -41
  247. package/src/modules/skills/skills.tools.ts +80 -8
  248. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  249. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  250. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  251. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  252. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  253. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  254. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  255. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  256. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  257. package/src/modules/tool-helpers/validate-token.ts +1 -1
  258. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  259. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  260. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  261. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  262. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  263. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  264. package/src/modules/workflow/file-lock.service.ts +31 -0
  265. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  266. package/src/modules/workflow/git/git.service.ts +168 -0
  267. package/src/modules/workflow/pending-commits.service.ts +59 -1
  268. package/src/modules/workflow/workflow-hooks.ts +64 -26
  269. package/src/modules/workflow/workflow.service.ts +121 -4
  270. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  271. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  272. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  273. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  274. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  275. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  276. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  277. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  278. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  279. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  280. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  281. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
  283. package/src/modules/workspace/agent-access.gate.ts +164 -0
  284. package/src/modules/workspace/routine-write-policy.ts +5 -6
  285. package/src/modules/workspace/session-sink.ts +5 -5
  286. package/src/modules/workspace/set-aside-clone.ts +96 -0
  287. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  288. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  289. package/src/modules/workspace/workspace.routes.ts +95 -3
  290. package/src/modules/workspace/workspace.service.ts +256 -24
  291. package/src/modules/workspace/workspace.tools.ts +338 -119
  292. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  293. package/src/modules/write-access/write-access.ts +153 -0
  294. package/src/shared/domain-errors.ts +31 -0
  295. package/src/shared/git.contract.ts +27 -0
  296. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  297. package/src/tenancy/static-tenant-source.ts +0 -3
  298. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  299. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  300. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  301. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  302. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  303. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  304. package/dist/modules/workflow/session-ontology.service.js +0 -147
  305. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  306. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  307. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  308. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  309. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  310. package/dist/shared/kb-layout.d.ts +0 -39
  311. package/dist/shared/kb-layout.d.ts.map +0 -1
  312. package/dist/shared/kb-layout.js +0 -103
  313. package/dist/shared/kb-layout.js.map +0 -1
  314. package/dist/shared/kb-layout.test.d.ts +0 -2
  315. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  316. package/dist/shared/kb-layout.test.js +0 -75
  317. package/dist/shared/kb-layout.test.js.map +0 -1
  318. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  319. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  320. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  321. package/src/modules/workflow/session-ontology.service.ts +0 -183
  322. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  323. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  324. package/src/shared/kb-layout.test.ts +0 -98
  325. package/src/shared/kb-layout.ts +0 -102
@@ -37,7 +37,7 @@ import {
37
37
  registerPluginsTools,
38
38
  } from '../modules/plugins/index.js';
39
39
  import { keyOrSessionAuth } from '../modules/tool-auth/key-or-session.middleware.js';
40
- import type { SessionOntologyGate } from '../modules/workspace/session-ontology.gate.js';
40
+ import { ToolDescriptionNotes, type AgentAccessGate } from '../modules/workspace/agent-access.gate.js';
41
41
  import {
42
42
  createSecretsVaultRoutes,
43
43
  createSecretsVaultPublicRoutes,
@@ -72,6 +72,7 @@ import { publicConfig } from './public-config.js';
72
72
  import { createReadiness } from './readiness.js';
73
73
  import { createAgentInstructionsRoutes } from '../modules/agent-instructions/index.js';
74
74
  import type { CoreServices } from './create-core-services.js';
75
+ import { createWriteAccessRoutes, createWriteGateMiddleware } from '../modules/write-access/write-access.js';
75
76
 
76
77
  type ExpressApp = ReturnType<typeof express>;
77
78
 
@@ -85,8 +86,8 @@ export interface ToolSurfaceCtx {
85
86
  router: Router;
86
87
  toolAuth: RequestHandler;
87
88
  toolHandler: CoreServices['toolHandlerFactory'];
88
- /** Shared ontology-session boundary gate config (file tools + graph tools). */
89
- sessionOntologyGate: SessionOntologyGate;
89
+ /** Shared agent-access gate config (file tools + graph tools). */
90
+ agentAccessGate: AgentAccessGate;
90
91
  core: CoreServices;
91
92
  }
92
93
 
@@ -200,6 +201,14 @@ export async function createCoreServer(
200
201
  // is mounted again below, once the body is parsed and once the caller is
201
202
  // known (see the two mounts under `/api/workspace/:id`).
202
203
  app.use('/api/workspace/:id', createGitInternalsRouteGuard(core.workspaceService));
204
+ // The read-only gate, ahead of every route — core's, the tool surface's
205
+ // and an overlay's alike — so a read-only deployment refuses a change
206
+ // wherever it would enter. It lets through what signs people in, manages
207
+ // accounts and configures the deployment (see `ALWAYS_WRITABLE`); write
208
+ // tools are judged by the tool layer. A no-op unless a host fills
209
+ // `ports.writeAccess`. Ahead of the body parser too: a refused write is
210
+ // refused with its own 403, never parsed first (and answered 400 or 413).
211
+ app.use(createWriteGateMiddleware(core.writeAccess));
203
212
  app.use((req, res, next) => {
204
213
  if (jsonExemptPaths.has(req.path) || isSyncRawBodyPath(req.path)) return next();
205
214
  return globalJson(req, res, next);
@@ -420,24 +429,23 @@ export async function createCoreServer(
420
429
  },
421
430
  }),
422
431
  );
423
- // Shared ontology-session boundary gate config, consumed by every tool
424
- // surface that touches the KB (file tools + graph tools). The gate's
425
- // blocking decision runs through the workflow hooks: core registers none
426
- // (tracking only); the enterprise root registers the ontology block on
427
- // `workflowService.hooks` before this server is built.
428
- const sessionOntologyGate = {
429
- service: core.sessionOntologyService,
430
- enabled: core.config.ontologySessionBlock,
431
- kb: core.kb,
432
+ // Shared agent-access gate config, consumed by every tool surface that
433
+ // touches the KB (file tools + graph tools). The gate decides nothing
434
+ // itself: it calls the read and write hooks a deployment registered on
435
+ // `workflowService.hooks`, and carries the notes that deployment wants
436
+ // agents to read about the gated tools. Core registers neither, so a
437
+ // core-only deployment refuses nothing and says nothing extra.
438
+ const agentAccessGate: AgentAccessGate = {
432
439
  recoveryBotEmail: RECOVERY_BOT_EMAIL,
433
440
  hooks: core.workflowService.hooks,
441
+ notes: new ToolDescriptionNotes(),
434
442
  };
435
443
  // A skill's `allowed-tools`, checked against what the caller can see — on
436
444
  // every save surface (agent write tools, the app's PUT /file) and on
437
445
  // `get_skill`. Warnings only; it never refuses a save.
438
446
  const allowedToolsChecker = new AllowedToolsChecker(core.toolRegistry, core.toolManualService, core.kb);
439
447
  registerWorkflowTools(core.toolRegistry, toolsRouter, ta, th, core.kb);
440
- registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, sessionOntologyGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate);
448
+ registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate);
441
449
  registerSkillsTools(core.toolRegistry, toolsRouter, ta, th, core.skillService, allowedToolsChecker);
442
450
  // Definitions only: the endpoints they describe are the app's own plugin
443
451
  // creation routes, mounted below behind the key-or-session gate.
@@ -455,7 +463,7 @@ export async function createCoreServer(
455
463
  router: toolsRouter,
456
464
  toolAuth: ta,
457
465
  toolHandler: th,
458
- sessionOntologyGate,
466
+ agentAccessGate,
459
467
  core,
460
468
  });
461
469
  toolsRouter.use(createManualRoutes(
@@ -546,6 +554,7 @@ export async function createCoreServer(
546
554
  // request also gets the resolved form judged — a link in the repository that
547
555
  // points into the git folder — before any read gate or lock.
548
556
  app.use('/api/workspace/:id', core.authMiddleware, createGitInternalsRouteGuard(core.workspaceService));
557
+ app.use('/api', core.authMiddleware, createWriteAccessRoutes(core.writeAccess));
549
558
  app.use('/api', core.authMiddleware, createWorkspaceRoutes(
550
559
  core.workspaceService,
551
560
  core.authService,
@@ -722,6 +731,15 @@ export async function createCoreServer(
722
731
  ensureManaged: (branch) => core.managedRepository.ensure(core.gitRunner, branch),
723
732
  githubApp: core.githubApp,
724
733
  },
734
+ // What a move to another repository has to decide about, and what it
735
+ // runs under. Bound here rather than handed the whole workflow service:
736
+ // the setup routes ask two questions, hold the commit worker for the
737
+ // move, and know nothing else about a request or a commit.
738
+ {
739
+ countOpen: () => core.workflowService.countOpenChangeRequests(),
740
+ closeAsRepositoryReplaced: () => core.workflowService.closeOpenChangeRequestsAsRepositoryReplaced(),
741
+ whileCommitsHeld: (work) => core.whileCommitsHeld(work),
742
+ },
725
743
  ),
726
744
  );
727
745
  // Connecting a repository on GitHub through a GitHub App: the round trips
@@ -12,6 +12,7 @@ import { sql } from 'drizzle-orm';
12
12
  import { DEFAULT_DB_SCHEMA, assertSearchPath } from '../modules/database/connection.js';
13
13
  import { DEFAULT_SECRETS_SCOPE } from '../modules/secrets-vault/secrets-variable-loader.js';
14
14
  import { KbContext } from '../shared/kb-context.js';
15
+ import { branchForWorkspaceId } from '../shared/workspace-id.js';
15
16
  import { getDb, type Database } from '../modules/database/connection.js';
16
17
  import { runCoreMigrations } from '../modules/database/migrate.js';
17
18
  import { coreMigrationsDir } from '../assets.js';
@@ -35,7 +36,7 @@ import { ManagedRepository, MANAGED_DEFAULT_BRANCH } from '../modules/settings/m
35
36
  import { RepositorySource } from '../modules/settings/repository-source.js';
36
37
  import { GitHubAppConnection } from '../modules/github-app/index.js';
37
38
  import { AdvisoryLease, AdvisoryLock } from '../modules/database/advisory-lock.js';
38
- import { holdCommitWorkerLease, withStartupTask, type LeaseLoopHandle } from './lifecycle.js';
39
+ import { holdCommitWorkerLease, holdable, withStartupTask, type LeaseLoopHandle } from './lifecycle.js';
39
40
 
40
41
  /** The hosted MCP endpoint at a deployment address, with any userinfo stripped. */
41
42
  function mcpEndpointUrl(publicBackendUrl: string): string {
@@ -110,7 +111,6 @@ import { WorkflowEventBus } from '../modules/workflow/event-bus.js';
110
111
  import { FileChangeNotifier } from '../modules/kb-fs/file-change-notifier.js';
111
112
  import { WorkflowService } from '../modules/workflow/workflow.service.js';
112
113
  import { WorkflowHooks } from '../modules/workflow/workflow-hooks.js';
113
- import { SessionOntologyService } from '../modules/workflow/session-ontology.service.js';
114
114
  import { PendingCommitsService } from '../modules/workflow/pending-commits.service.js';
115
115
  import {
116
116
  PendingCommitsWorker,
@@ -142,6 +142,7 @@ import { BevelOAuthProvider } from '../modules/mcp/oauth/bevel-oauth-provider.js
142
142
  import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
143
143
  import { ToolRegistry } from '../modules/tool-registry/tool-registry.js';
144
144
  import { createToolContextResolver } from '../modules/tool-helpers/tool-context.js';
145
+ import { alwaysWritable, type IWriteAccess } from '../modules/write-access/write-access.js';
145
146
  import { createToolHandlerFactory } from '../modules/tool-helpers/tool-handler.js';
146
147
  import { TokenCrypto } from '../shared/token-crypto.js';
147
148
  import { UpdateCheckService } from '../modules/update-check/update-check.service.js';
@@ -182,6 +183,12 @@ export interface CoreServices {
182
183
  * lease to the replacement.
183
184
  */
184
185
  commitWorker: LeaseLoopHandle;
186
+ /**
187
+ * Run something with no queued commit being written: the commit worker is
188
+ * stopped for its duration and started again afterwards, the lease kept.
189
+ * What a move to another repository runs the startup phase under.
190
+ */
191
+ whileCommitsHeld<T>(work: () => Promise<T>): Promise<T>;
185
192
  workspaceService: WorkspaceService;
186
193
  /**
187
194
  * The KB startup phase (see `startup/on-server-start.ts`): run at the
@@ -218,7 +225,6 @@ export interface CoreServices {
218
225
  creatorAccess: CreatorAccessService;
219
226
  /** Read-before-write, for the surfaces that ask ahead of the lock (see `access-model/change-gate.ts`). */
220
227
  changeGate: ChangeReadGate;
221
- sessionOntologyService: SessionOntologyService;
222
228
  routineWritePolicy: RoutineWritePolicyService;
223
229
  skillService: SkillService;
224
230
  pendingSkillsService: PendingSkillsService;
@@ -327,6 +333,8 @@ export interface CoreServices {
327
333
  toolAuthMiddleware: ReturnType<typeof createToolAuthMiddleware>;
328
334
  manualAuthMiddleware: ReturnType<typeof createManualAuthMiddleware>;
329
335
  toolHandlerFactory: ReturnType<typeof createToolHandlerFactory>;
336
+ /** Whether the deployment may be changed right now — `ports.writeAccess`, or always. */
337
+ writeAccess: IWriteAccess;
330
338
  // ── Server-time seams (enterprise overwrites after construction) ────────
331
339
  /** `start_session` backing — core default {@link UuidSessionSink}. */
332
340
  sessionSink: ISessionSink;
@@ -486,12 +494,83 @@ export async function createCoreServices(
486
494
  if (repositorySource.mode() === 'github-app') {
487
495
  await githubApp.prepare().catch(() => undefined);
488
496
  }
497
+ /**
498
+ * Where a working copy of a replaced repository is kept, for BOTH the places
499
+ * that find one: the KB startup phase sweeping the workspaces root, and the
500
+ * workspace service refusing to adopt one on a branch open. One variable,
501
+ * passed to both, because they move the same clones for the same reason and
502
+ * an admin looking for their work must have one folder to look in — the two
503
+ * drifted apart once already, and the copy the workspace service set aside
504
+ * landed in the image's own filesystem and was gone at the next recreate.
505
+ *
506
+ * Under the backups root: a persistent volume of its own, and one nothing
507
+ * sweeps (see the runner's `reconcileClonesWithConfiguredRepository`).
508
+ */
509
+ const replacedWorkingCopiesRoot = path.join(config.backupsRoot, 'replaced-working-copies');
510
+ /**
511
+ * What must leave with a working copy that is set aside, whoever sets it
512
+ * aside: the startup phase, or the workspace service on a branch open.
513
+ *
514
+ * Its queue: whatever still waits to be committed into that copy was
515
+ * written against the repository that was left, and the path it names is
516
+ * about to hold a fresh clone of another one. Held for a person, on every
517
+ * branch, never written and never deleted.
518
+ *
519
+ * And the locks on its branch: each is a claim on a file of the copy that
520
+ * is going. Left standing, a holder still connected keeps one alive with
521
+ * heartbeats, and the same path in the new repository refuses everybody
522
+ * else. Dropped without enqueueing anything, as when change requests are
523
+ * closed as "repository replaced".
524
+ *
525
+ * Throws when either cannot be done, and the caller then leaves the copy
526
+ * where it is. Reaches FORWARD to both services, like `gitService` below:
527
+ * it is only ever called long after this function has returned them.
528
+ */
529
+ const releaseWorkOnSetAsideCopy = async (workspaceId: string): Promise<void> => {
530
+ const held = await pendingCommitsService.markNeedsAttentionInWorkspace(
531
+ workspaceId,
532
+ 'The knowledge-base repository was replaced while this commit was still queued, so it was never ' +
533
+ 'written. The bytes are kept here: the working copy it was meant for belongs to the previous repository.',
534
+ );
535
+ if (held > 0) {
536
+ logger('kb-startup').warn(
537
+ `${held} queued commit(s) for the working copy "${workspaceId}" need attention: the repository was ` +
538
+ 'replaced before they landed, so they were not written to the new one.',
539
+ );
540
+ }
541
+ await fileLockService.releaseAllOnBranch(branchForWorkspaceId(workspaceId));
542
+ };
489
543
  const workspaceService = new WorkspaceService(
490
544
  config.workspacesRoot,
491
545
  () => repositorySource.url(),
492
546
  kb,
493
547
  disk,
494
548
  gitRunner,
549
+ replacedWorkingCopiesRoot,
550
+ // A branch opened onto a clone of another repository sets it aside on
551
+ // the spot, with nobody having stopped the commit worker for it. So it
552
+ // is held here, for the one move, and the copy's work leaves first and
553
+ // is looked for again once the copy has gone: a release that queued its
554
+ // bytes in between was writing to the copy that left.
555
+ //
556
+ // Only the first look may refuse the branch: the copy has not moved yet.
557
+ // The second runs after the move, so its failure is tried once more and
558
+ // then logged, as the startup phase logs the same step. Refusing then
559
+ // would report a move that did happen as one that did not.
560
+ (workspaceId, move) =>
561
+ leased.whileHeld(async () => {
562
+ await releaseWorkOnSetAsideCopy(workspaceId);
563
+ await move();
564
+ await releaseWorkOnSetAsideCopy(workspaceId)
565
+ .catch(() => releaseWorkOnSetAsideCopy(workspaceId))
566
+ .catch((err: unknown) => {
567
+ logger('workspace').error(
568
+ `the working copy "${workspaceId}" was set aside, but what was queued or locked on it meanwhile ` +
569
+ 'could not be looked for again:',
570
+ { err },
571
+ );
572
+ });
573
+ }),
495
574
  );
496
575
  // The KB startup phase: every seeding, scaffolding and migration concern,
497
576
  // run through one runner at the deployment's quiet moments (boot + setup
@@ -529,10 +608,9 @@ export async function createCoreServices(
529
608
  // the first thing that needs them.
530
609
  kbRepoUrl: () => repositorySource.url(),
531
610
  workspacesRoot: config.workspacesRoot,
532
- // Under the backups root, a persistent volume of its own and one nothing
533
- // sweeps: a working copy of a repository that was replaced is kept there,
534
- // never deleted (see the runner's `reconcileClonesWithConfiguredRepository`).
535
- setAsideRoot: path.join(config.backupsRoot, 'replaced-working-copies'),
611
+ // The same folder the workspace service sets aside into — see
612
+ // `replacedWorkingCopiesRoot` above for why the two must agree.
613
+ setAsideRoot: replacedWorkingCopiesRoot,
536
614
  kbDirName,
537
615
  templateDir: config.kbTemplateDir,
538
616
  defaultBranch: () => kb.defaultBranch,
@@ -543,6 +621,33 @@ export async function createCoreServices(
543
621
  steps: kbStartupSteps,
544
622
  buildSeedTree: buildSeedTree(disk, config.kbTemplateDir, extraDirs, [config.adminEmail], kb),
545
623
  gitRunner,
624
+ // The work on a working copy that is about to be set aside leaves first:
625
+ // its queued commits and the locks on its branch (see
626
+ // `releaseWorkOnSetAsideCopy`). A failure here stops the phase before
627
+ // the copy moves: a replacement cloned over an untouched queue is how
628
+ // those bytes would land in the wrong repository.
629
+ beforeCloneSetAside: releaseWorkOnSetAsideCopy,
630
+ // And once it is set aside it must leave the workspace service's cache:
631
+ // on the SAVE that moves the deployment the process is already running,
632
+ // and a cached path to a directory that is gone is how the next reader
633
+ // gets an ENOENT instead of a fresh clone.
634
+ //
635
+ // Then the copy's work is looked for once more. A release that queued
636
+ // its bytes between the first look and the move was writing to the copy
637
+ // that has now gone; nothing can queue against it after this.
638
+ onCloneDiscarded: async (workspaceId) => {
639
+ workspaceService.forgetClone(workspaceId);
640
+ await releaseWorkOnSetAsideCopy(workspaceId);
641
+ },
642
+ // And the replacement it cloned in its place: a fresh clone holds every
643
+ // ref, so the git layer's per-workspace fetch record is told so. Without
644
+ // it that record still holds the FAILED fetch of the repository that was
645
+ // replaced, and a strict branch listing within its TTL refuses the new
646
+ // clone's refs as unproven. Reaches FORWARD to `gitService`, which is
647
+ // built further down: safe because the runner only ever calls this from
648
+ // `runAll`, which the server builder invokes long after this function has
649
+ // returned every service.
650
+ onCloneCreated: (workspaceId) => gitService.noteWorkspaceFetched(workspaceId),
546
651
  });
547
652
  // Shared, workspace-independent store for oversized `call_tool_chain` results,
548
653
  // read back via `read_file`. Sibling of `workspacesRoot`, never committed.
@@ -574,14 +679,10 @@ export async function createCoreServices(
574
679
  // except that new folder at a root (see `access-model/change-gate.ts`).
575
680
  const changeGate = new ChangeReadGate(workspaceService, accessControl, kb, disk);
576
681
 
577
- // Ontology-session boundary: records each agent run's touched ontologies and
578
- // blocks writes once a run has crossed ontologies. Postgres-backed so the
579
- // boundary survives a restart.
580
- const sessionOntologyService = new SessionOntologyService(db, kb);
581
682
  // Per-run write restriction (by file extension). Shared by the workspace tool
582
683
  // surface (which enforces it) and the routine runner (which sets it for
583
- // dashboard-only `watchlist_check` runs). In-memory: a restriction lives only for
584
- // one run, unlike the Postgres-backed ontology touched-set above.
684
+ // dashboard-only `watchlist_check` runs). In-memory: a restriction lives only
685
+ // for one run.
585
686
  const routineWritePolicy = new RoutineWritePolicyService();
586
687
  // Skills: discovered from the default-branch workspace only (global catalog).
587
688
  const skillService = new SkillService(workspaceService, accessControl, kb, disk);
@@ -632,11 +733,11 @@ export async function createCoreServices(
632
733
  // against each other — a backup-reseed races a concurrent commit otherwise.
633
734
  const workspaceMutex = new WorkspaceMutex();
634
735
  // Workflow lifecycle hooks — the ONE registry this composition shares
635
- // between GitService (advisory commit validation), the session-ontology
636
- // gate (blocking preWrite), and the enterprise composition root, which
637
- // registers module-owned handlers on `workflowService.hooks` right after
638
- // this function returns. Core registers none: no commit-time validation
639
- // (advisory anyway) and no ontology write block.
736
+ // between GitService (advisory commit validation), the agent-access gate
737
+ // (the blocking agentRead / preWrite hooks), and the composition root,
738
+ // which registers module-owned handlers on `workflowService.hooks` right
739
+ // after this function returns. Core registers none: no commit-time
740
+ // validation (advisory anyway) and nothing that refuses an agent call.
640
741
  const workflowHooks = new WorkflowHooks();
641
742
  const gitService = new GitService(
642
743
  workspaceService,
@@ -731,7 +832,7 @@ export async function createCoreServices(
731
832
  eventBus,
732
833
  fileChangeNotifier,
733
834
  // Exposed as `workflowService.hooks` — the SAME instance GitService and
734
- // the session-ontology gate consult, so enterprise registrations against
835
+ // the agent-access gate consult, so a deployment's registrations against
735
836
  // it reach every hook point.
736
837
  workflowHooks,
737
838
  );
@@ -1134,8 +1235,9 @@ export async function createCoreServices(
1134
1235
  creatorAccess,
1135
1236
  loadActiveGroups,
1136
1237
  });
1137
- const toolHandlerFactory = createToolHandlerFactory(resolveToolContext);
1138
- const toolAuthMiddleware = createToolAuthMiddleware(externalApiKeyService, internalTokenService);
1238
+ const writeAccess = ports.writeAccess ?? alwaysWritable;
1239
+ const toolHandlerFactory = createToolHandlerFactory(resolveToolContext, writeAccess);
1240
+ const toolAuthMiddleware = createToolAuthMiddleware(externalApiKeyService, internalTokenService, authService);
1139
1241
  // Read-only manual endpoints accept the above PLUS a browser JWT, so a
1140
1242
  // logged-in user can browse the catalog with their session. Execution routes
1141
1243
  // keep `toolAuthMiddleware` (no JWT), so a session can read but not invoke.
@@ -1187,7 +1289,10 @@ export async function createCoreServices(
1187
1289
  name: recoveryBot.name,
1188
1290
  },
1189
1291
  });
1190
- const leased = withStartupTask(pendingCommitsWorker, reconcileQueue);
1292
+ // Holdable: a move to another repository stops the worker for as long as
1293
+ // working copies are being set aside and cloned again, without giving up
1294
+ // the lease (see `holdable` in `core/lifecycle.ts`).
1295
+ const leased = holdable(withStartupTask(pendingCommitsWorker, reconcileQueue));
1191
1296
 
1192
1297
  // SSO providers. The array REFERENCE is shared with the caller's port — an
1193
1298
  // overlay pushes its own plugins into it after construction (they mount when
@@ -1283,6 +1388,7 @@ export async function createCoreServices(
1283
1388
  db,
1284
1389
  gitRunner,
1285
1390
  commitWorker,
1391
+ whileCommitsHeld: (work) => leased.whileHeld(work),
1286
1392
  startupRetry: null,
1287
1393
  tenantKey,
1288
1394
  secretsScope,
@@ -1302,7 +1408,6 @@ export async function createCoreServices(
1302
1408
  accessControl,
1303
1409
  creatorAccess,
1304
1410
  changeGate,
1305
- sessionOntologyService,
1306
1411
  routineWritePolicy,
1307
1412
  skillService,
1308
1413
  pendingSkillsService,
@@ -1353,6 +1458,7 @@ export async function createCoreServices(
1353
1458
  toolAuthMiddleware,
1354
1459
  manualAuthMiddleware,
1355
1460
  toolHandlerFactory,
1461
+ writeAccess,
1356
1462
  // Server-time seams — defaults here; the enterprise overlay overwrites
1357
1463
  // (or, for the array, pushes into) these after construction.
1358
1464
  sessionSink: ports.sessionSink ?? new UuidSessionSink(),
@@ -1,7 +1,7 @@
1
1
  import type { Server } from 'node:http';
2
2
  import { closeDb, type Database } from '../modules/database/connection.js';
3
3
  import type { AdvisoryLease } from '../modules/database/advisory-lock.js';
4
- import { KbRemoteUnreachableError, type KbStartupRunner } from '../modules/workspace/startup/kb-startup-runner.js';
4
+ import { bootMaySurvive, type KbStartupRunner } from '../modules/workspace/startup/kb-startup-runner.js';
5
5
  import { noteBesideCheckout } from '../modules/workspace/startup/beside-checkout.js';
6
6
  import { unregisterBevelSecretsVariableLoader } from '../modules/secrets-vault/secrets-variable-loader.js';
7
7
  import type { WorkflowService } from '../modules/workflow/workflow.service.js';
@@ -164,7 +164,13 @@ export function holdCommitWorkerLease(
164
164
  running = false;
165
165
  wake?.();
166
166
  await loop;
167
+ // The worker is asked to stop whether or not this process is running
168
+ // it: a worker that can be held still (`holdable`) may be in the
169
+ // middle of a hold that uses the database, and its stop is what
170
+ // waits for that. Stopping a worker that is not running is nothing.
171
+ const idle = !workerRunning && !stopping;
167
172
  await stopWorker();
173
+ if (idle) await worker.stop().catch((err: unknown) => log(`commit worker stop failed: ${String(err)}`));
168
174
  await lease.release();
169
175
  })();
170
176
  return stopped;
@@ -202,6 +208,80 @@ export function withStartupTask(
202
208
  };
203
209
  }
204
210
 
211
+ /** A worker the lease loop drives, which something else may hold still for a while. */
212
+ export interface HoldableWorker extends LeasedWorker {
213
+ /**
214
+ * Run `work` with the worker stopped, and start it again afterwards if the
215
+ * lease loop still wants it running. A hold waits for the commit in flight
216
+ * to finish, as a stop does. Holds may overlap; the worker starts again
217
+ * when the last one ends, whether `work` resolved or threw.
218
+ */
219
+ whileHeld<T>(work: () => Promise<T>): Promise<T>;
220
+ }
221
+
222
+ /**
223
+ * A worker that can be HELD STILL without the lease loop knowing.
224
+ *
225
+ * The lease loop decides whether this process is the one draining the commit
226
+ * queue; it starts the worker when the lease is taken and stops it when the
227
+ * lease is lost. Moving the deployment to another repository needs a second,
228
+ * independent reason for the worker not to run: while working copies are
229
+ * being set aside and cloned again, a queued commit must not be written into
230
+ * a directory that is being renamed away, nor into the fresh clone of a
231
+ * repository it was never meant for. Stopping the loop itself would release
232
+ * the lease, and another process would take the queue over mid-move.
233
+ *
234
+ * So the two reasons are kept apart. `wanted` is the lease loop's word,
235
+ * `holds` counts the moves in flight, and the worker runs exactly when it is
236
+ * wanted and nothing holds it.
237
+ *
238
+ * A stop waits for the holds in flight as well as for the worker. Shutdown
239
+ * awaits this stop (bounded by its own budget) before it ends the database
240
+ * pool, and a move still setting copies aside and cloning needs that pool:
241
+ * it is either finished, or known to have been cut.
242
+ */
243
+ export function holdable(worker: LeasedWorker): HoldableWorker {
244
+ let wanted = false;
245
+ const inFlight = new Set<Promise<unknown>>();
246
+ /**
247
+ * ONE stop at a time, shared by everyone who asks. A worker's own stop is
248
+ * idempotent by answering a second caller at once, while the first is
249
+ * still waiting for the commit in flight; a second hold given that answer
250
+ * would set a working copy aside under that commit.
251
+ */
252
+ let stopping: Promise<void> | null = null;
253
+ const stopWorker = (): Promise<void> => {
254
+ stopping ??= worker.stop().finally(() => {
255
+ stopping = null;
256
+ });
257
+ return stopping;
258
+ };
259
+ return {
260
+ start() {
261
+ wanted = true;
262
+ if (inFlight.size === 0) worker.start();
263
+ },
264
+ async stop() {
265
+ wanted = false;
266
+ await stopWorker();
267
+ await Promise.allSettled([...inFlight]);
268
+ },
269
+ whileHeld<T>(work: () => Promise<T>): Promise<T> {
270
+ const held = (async () => {
271
+ await stopWorker();
272
+ return work();
273
+ })();
274
+ inFlight.add(held);
275
+ const ended = (): void => {
276
+ inFlight.delete(held);
277
+ if (inFlight.size === 0 && wanted) worker.start();
278
+ };
279
+ held.then(ended, ended);
280
+ return held;
281
+ },
282
+ };
283
+ }
284
+
205
285
  /**
206
286
  * What one knowledge base's graph needs of itself to be STARTED: the boot
207
287
  * side effects that used to run inside `createCoreServer`, named so a host
@@ -228,7 +308,8 @@ export interface BootableCore {
228
308
  * jobs. `createCoreServer` runs this for a single-tenant deployment; a host
229
309
  * serving several knowledge bases runs it when a tenant is activated.
230
310
  *
231
- * Throws to stop the boot on every failure but an unreachable remote — the
311
+ * Throws to stop the boot on every failure but the ones the host answered
312
+ * about the repository or the credentials (see `bootMaySurvive`) — the
232
313
  * container's restart policy is the retry — see `kb-startup-runner.ts`.
233
314
  */
234
315
  export async function startCore<C extends BootableCore>(
@@ -267,18 +348,22 @@ export async function startCore<C extends BootableCore>(
267
348
  try {
268
349
  await core.kbStartupRunner.runAll();
269
350
  } catch (err) {
270
- // The one failure a boot survives: the remote cannot be reached. That
271
- // says nothing about the knowledge base — what we would write is not
272
- // known to be wrong, we cannot get there — so refusing to boot only took
273
- // away the login and setup screens an operator needs to fix it (a
274
- // rotated token, say). The deployment comes up GATED: the setup routes
351
+ // The failures a boot survives: the host ANSWERED about the repository or
352
+ // the credentials, or could not be reached at all. None of them says
353
+ // anything about the knowledge base — what we would write is not known to
354
+ // be wrong — and every one of them is fixed somewhere else: the host comes
355
+ // back, the repository is created, the token is granted access, a rotated
356
+ // token is typed into the setup screen. Refusing to boot took away the
357
+ // login and setup screens the fix is entered on, and on 2026-09-28 a
358
+ // repository that had been replaced crash-looped a deployment for exactly
359
+ // that reason. The deployment comes up GATED instead: the setup routes
275
360
  // read the runner's standing failure and keep the app shut, the setup
276
- // screen shows why, saving it retries, and the runner keeps trying on
277
- // its own. Every other failure still stops the boot, because it means
278
- // the template or a step would write something wrong.
279
- if (!(err instanceof KbRemoteUnreachableError)) throw err;
280
- startupLog.error('booting UNMAINTAINED and gated — the knowledge-base remote could not be reached:', {
281
- detail: err.message,
361
+ // screen shows why, saving it retries, and the runner keeps trying on its
362
+ // own. Every other failure still stops the boot, because it means the
363
+ // template or a step would write something wrong. See `bootMaySurvive`.
364
+ if (!bootMaySurvive(err)) throw err;
365
+ startupLog.error('booting UNMAINTAINED and gated — the knowledge base could not be initialized:', {
366
+ detail: err instanceof Error ? err.message : String(err),
282
367
  });
283
368
  // Kept on the graph so that stopping the graph stops the asking: a
284
369
  // retry that outlived its graph would clone into a workspaces folder
@@ -6,6 +6,8 @@ import { assertKeyDecodesTo32Bytes } from './shared/token-crypto.js';
6
6
  import { DEFAULT_GIT_TIMEOUT_MS } from './modules/workflow/git/node-git-runner.js';
7
7
  import { DEFAULT_DB_SCHEMA, assertSchemaName } from './modules/database/connection.js';
8
8
  import { logger } from './shared/logging.js';
9
+ import { hasControlCharacter } from './modules/kb-fs/repo-path.js';
10
+ import { sanitizedPath } from './shared/printable.js';
9
11
 
10
12
  const log = logger('config');
11
13
 
@@ -100,7 +102,6 @@ export interface TenantConfig {
100
102
  readonly gitUsername: string;
101
103
  readonly gitToken: string;
102
104
  readonly kbTemplateDir: string;
103
- readonly ontologySessionBlock: boolean;
104
105
  readonly updateCheckEnabled: boolean;
105
106
  readonly loginPasswordEnabled: boolean;
106
107
  readonly allowedEmailDomains: string[];
@@ -259,17 +260,6 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
259
260
  * existing one. Override with `KB_TEMPLATE_DIR`.
260
261
  */
261
262
  readonly kbTemplateDir: string;
262
- /**
263
- * Ontology-session boundary kill-switch. When true (default), an agent run
264
- * that has read across more than one ontology can no longer write. A no-op on
265
- * a single-ontology KB. Set `ONTOLOGY_SESSION_BLOCK=false` to disable.
266
- *
267
- * Stays on CORE config (not `AppConfig`) even though the write BLOCK itself
268
- * is an enterprise-registered hook: the flag also switches off the core-owned
269
- * touch TRACKING (the gate skips entirely when false — see
270
- * `session-ontology.gate.ts`), so it must exist wherever the tracking runs.
271
- */
272
- readonly ontologySessionBlock: boolean;
273
263
  /**
274
264
  * In-app update check. When true (default), `GET /api/update-check` lazily
275
265
  * asks api.github.com for the newest Hexis release — only when an admin's
@@ -469,8 +459,6 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
469
459
  // Default: the `kb-template/` folder shipped inside this package (works
470
460
  // both from src/ and compiled dist/ — see assets.ts).
471
461
  this.kbTemplateDir = process.env.KB_TEMPLATE_DIR || defaultKbTemplateDir();
472
- this.ontologySessionBlock =
473
- (process.env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false';
474
462
  this.updateCheckEnabled =
475
463
  (process.env.UPDATE_CHECK ?? 'true').trim().toLowerCase() !== 'false';
476
464
  this.allowedEmailDomains = (process.env.ALLOWED_EMAIL_DOMAINS || '')
@@ -621,5 +609,16 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
621
609
  ) {
622
610
  throw new Error(`KB_DIR_NAME must be a single path segment: ${this.kbDirName}`);
623
611
  }
612
+ // A control character, a line break above all: this name is PREFIXED onto
613
+ // every workspace-relative path, and a line break in a path is a separator
614
+ // in git's line-oriented stdin protocols (`cat-file --batch` reads one
615
+ // `<ref>:<path>` per line). A name carrying one would make the normaliser
616
+ // emit the very path it refuses from a caller — so the deployment fails to
617
+ // boot rather than resolving paths nothing downstream can safely read.
618
+ if (hasControlCharacter(this.kbDirName)) {
619
+ throw new Error(
620
+ `KB_DIR_NAME must not contain a control character: "${sanitizedPath(this.kbDirName)}"`,
621
+ );
622
+ }
624
623
  }
625
624
  }
package/src/index.ts CHANGED
@@ -101,8 +101,18 @@ export type {
101
101
  export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
102
102
  export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
103
103
  export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
104
+ export {
105
+ alwaysWritable,
106
+ READ_ONLY_CODE,
107
+ type IWriteAccess,
108
+ type WriteAccessVerdict,
109
+ } from './modules/write-access/write-access.js';
104
110
  export {
105
111
  AccountAdmissionRefusedError,
112
+ AccountDeactivatedError,
113
+ AccountChangeRefusedError,
114
+ AuthBackendError,
115
+ ACCOUNT_DEACTIVATED_MESSAGE,
106
116
  admitEveryone,
107
117
  type AccountAdmissionVerdict,
108
118
  type AccountProvisionReason,
@@ -112,6 +122,7 @@ export {
112
122
  WorkflowHooks,
113
123
  type CommitValidationHook,
114
124
  type CommitValidationContext,
125
+ type AgentOperationContext,
126
+ type AgentReadHook,
115
127
  type PreWriteHook,
116
- type PreWriteContext,
117
128
  } from './modules/workflow/workflow-hooks.js';