@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
@@ -1,11 +1,25 @@
1
1
  import type { Router, RequestHandler } from 'express';
2
2
  import type { IToolRegistry, UtcpTool } from '../tool-registry/tool.contract.js';
3
- import type { ToolContext } from '../tool-helpers/tool.contract.js';
3
+ import { ToolError, type ToolContext } from '../tool-helpers/tool.contract.js';
4
4
  import { toolDef } from '../tool-helpers/tool-def.js';
5
5
  import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
6
6
  import type { ISkillService } from './skills.contract.js';
7
7
  import type { IAllowedToolsChecker } from './allowed-tools-check.js';
8
8
 
9
+ /**
10
+ * The optional `branch` both skill tools declare. Optional on purpose, unlike
11
+ * the required `branch` of the KB file tools: a skill read is answered by the
12
+ * released catalog by default, and naming a branch is the caller asking for
13
+ * something nobody has approved yet.
14
+ */
15
+ const BRANCH_SKILLS_INPUT = {
16
+ type: 'string' as const,
17
+ description:
18
+ 'Optional: a draft branch to read the skills from instead of the released (default) branch — ' +
19
+ 'the branch you are working on. You must be able to read the branch and the skill on it; ' +
20
+ 'a branch that does not exist answers 404 naming it. Omit it for the approved skills.',
21
+ };
22
+
9
23
  /**
10
24
  * Registers the two skill tools (both surfaces) and hosts their endpoints.
11
25
  *
@@ -14,6 +28,13 @@ import type { IAllowedToolsChecker } from './allowed-tools-check.js';
14
28
  * what exists right in the tool catalog, no `list_skills` round-trip needed, and
15
29
  * it auto-updates as the default-branch catalog changes. The hosted routes are
16
30
  * static; only the description text is dynamic.
31
+ *
32
+ * Both tools take an optional `branch`. Without it they answer from the
33
+ * released (default-branch) catalog — what the descriptions name, what the
34
+ * browser menu lists and what the MCP prompt surface serves. With it they read
35
+ * the skills as they are on that draft and say which of them nobody has
36
+ * approved, so an agent can try the skill it just wrote without waiting for a
37
+ * merge.
17
38
  */
18
39
  export function registerSkillsTools(
19
40
  registry: IToolRegistry,
@@ -32,8 +53,8 @@ export function registerSkillsTools(
32
53
  router.post(
33
54
  '/agent/tools/list_skills',
34
55
  toolAuth,
35
- toolHandler(async (_args, ctx: ToolContext) => ({
36
- skills: await skillService.listSkills(ctx.user.email),
56
+ toolHandler(async (args, ctx: ToolContext) => ({
57
+ skills: await skillService.listSkills(ctx.user.email, { branch: branchArg(args) }),
37
58
  })),
38
59
  );
39
60
 
@@ -44,14 +65,38 @@ export function registerSkillsTools(
44
65
  const name = typeof args.name === 'string' ? args.name : '';
45
66
  const file = typeof args.file === 'string' ? args.file : undefined;
46
67
  const version = typeof args.version === 'string' ? args.version : undefined;
68
+ const branch = branchArg(args);
47
69
  if (!name) return { error: 'missing_name' };
48
- const result = await skillService.getSkill(ctx.user.email, name, file, { version });
70
+ // Refused rather than ranked: a `version` is a point in the released
71
+ // skill's history, a `branch` is a draft nobody has released — asked for
72
+ // together, neither answer is the one the caller meant, and guessing
73
+ // would serve instructions under a label that does not describe them.
74
+ if (branch !== undefined && version !== undefined && version.trim().length > 0) {
75
+ throw new ToolError(
76
+ 'get_skill takes `branch` or `version`, not both: `branch` loads the skill as that draft has it ' +
77
+ 'now, `version` loads a version the released skill declared. Pass one of them.',
78
+ 400,
79
+ );
80
+ }
81
+ const result = await skillService.getSkill(ctx.user.email, name, file, { version, branch });
49
82
  if (!allowedTools || !result.ok || result.kind !== 'skill') return result;
50
83
  return { ...result, warnings: await allowedTools.check(ctx.user.email, result.skill.allowedTools) };
51
84
  }),
52
85
  );
53
86
  }
54
87
 
88
+ /**
89
+ * The `branch` a tool call named, or undefined when it named none (the
90
+ * released catalog). A non-string or blank value is NOT a branch: it is taken
91
+ * as absent, which answers from the default branch — the approved one. The
92
+ * resolution of a real name, and the 404 for one the platform never heard of,
93
+ * belong to the workspace layer, exactly as on the file tools.
94
+ */
95
+ function branchArg(args: Record<string, unknown>): string | undefined {
96
+ const branch = args.branch;
97
+ return typeof branch === 'string' && branch.trim().length > 0 ? branch : undefined;
98
+ }
99
+
55
100
  /** "Currently available skills: `a`, `b`." (or a no-skills note), filtered to what the caller may read. */
56
101
  async function availableSkillsLine(skillService: ISkillService, userEmail?: string): Promise<string> {
57
102
  const skills = await skillService.listSkills(userEmail);
@@ -67,9 +112,16 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
67
112
  'for a skill that declares one, its current `version` (its SKILL.md `metadata.version`, else a ' +
68
113
  'top-level `version`, else `lifecycle.version`). ' +
69
114
  'Discover what skills exist before specialist work, then `get_skill` to load one. ' +
115
+ 'Pass `branch` to list the skills as they are on a draft branch instead of the released set — ' +
116
+ 'what you need to try a skill you just wrote there; each skill that differs from the released ' +
117
+ 'one comes back with `unmerged: true` and that branch, meaning nobody has approved it. ' +
70
118
  (await availableSkillsLine(skillService, userEmail)),
71
119
  path: '/api/agent/tools/list_skills',
72
- inputs: { type: 'object', properties: {}, additionalProperties: false },
120
+ inputs: {
121
+ type: 'object',
122
+ properties: { branch: BRANCH_SKILLS_INPUT },
123
+ additionalProperties: false,
124
+ },
73
125
  outputs: {
74
126
  type: 'object',
75
127
  properties: {
@@ -88,6 +140,13 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
88
140
  'else `lifecycle.version` in its SKILL.md); absent when it declares none.',
89
141
  },
90
142
  path: { type: 'string' },
143
+ unmerged: {
144
+ type: 'boolean',
145
+ description:
146
+ 'Only when `branch` was passed, and only on a skill that differs from the released one ' +
147
+ '(it exists only on that branch, or was changed there): nobody has approved what it says.',
148
+ },
149
+ branch: { type: 'string', description: 'With `unmerged`: the branch the skill was read from.' },
91
150
  },
92
151
  },
93
152
  },
@@ -104,7 +163,7 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
104
163
  'Load a skill by name: returns its full instructions (SKILL.md body) to follow, plus the skill ' +
105
164
  'folder path and the list of bundled files. Pass `file` to fetch a bundled file’s content ' +
106
165
  '(e.g. a script) instead of the body. Loads the latest copy unless `version` names an earlier ' +
107
- 'one the skill declared. ' +
166
+ 'one the skill declared, or `branch` names a draft to load it from. ' +
108
167
  (await availableSkillsLine(skillService, userEmail)),
109
168
  path: '/api/agent/tools/get_skill',
110
169
  inputs: {
@@ -126,6 +185,7 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
126
185
  'recent commit that declared that version; a version the skill never declared answers ' +
127
186
  '`version_not_found` with the versions it did declare.',
128
187
  },
188
+ branch: BRANCH_SKILLS_INPUT,
129
189
  },
130
190
  required: ['name'],
131
191
  additionalProperties: false,
@@ -134,8 +194,20 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
134
194
  type: 'object',
135
195
  description: 'On success carries `skill` (or `file` when `file` was passed); on failure carries `error`.',
136
196
  properties: {
137
- skill: { type: 'object', description: 'The loaded skill: name, description, body, files, ….' },
138
- file: { type: 'object', description: 'A bundled file: name, file, path, content.' },
197
+ skill: {
198
+ type: 'object',
199
+ description:
200
+ 'The loaded skill: name, description, body, files, …. Read from a `branch` that changed it, ' +
201
+ 'it also carries `unmerged: true` and that branch, and its `body` BEGINS with one line ' +
202
+ 'saying the skill comes from that unmerged branch and is not approved — treat it as a ' +
203
+ 'proposal you are testing, not as approved instructions.',
204
+ },
205
+ file: {
206
+ type: 'object',
207
+ description:
208
+ 'A bundled file: name, file, path, content — plus `unmerged` and `branch` when it came off ' +
209
+ 'a branch that changed the skill (the note stays beside the content, never inside it).',
210
+ },
139
211
  warnings: {
140
212
  type: 'array',
141
213
  description:
@@ -8,6 +8,7 @@ import { InternalTokenService } from '../internal-token.service.js';
8
8
  import { createManualAuthMiddleware } from '../tool-auth.middleware.js';
9
9
  import type { IExternalApiKeyService } from '../external-api-key.interface.js';
10
10
  import type { AuthService } from '../../auth/auth.service.js';
11
+ import { AuthBackendError } from '../../auth/account-admission.js';
11
12
 
12
13
  /**
13
14
  * The manual endpoints (`/agent/utcp`, `/agent/internal/utcp`) accept a browser
@@ -17,6 +18,8 @@ import type { AuthService } from '../../auth/auth.service.js';
17
18
  */
18
19
  const KEY = 'bevel_validkey';
19
20
  const JWT = 'browser-jwt';
21
+ /** A valid session, presented while its account cannot be looked up. */
22
+ const JWT_DURING_OUTAGE = 'browser-jwt-during-outage';
20
23
 
21
24
  const fakeExternalApiKeys = {
22
25
  looksLikeExternalApiKey: (t: string) => typeof t === 'string' && t.startsWith('bevel_'),
@@ -25,10 +28,12 @@ const fakeExternalApiKeys = {
25
28
  } as unknown as IExternalApiKeyService;
26
29
 
27
30
  const fakeAuth = {
28
- verifyToken: (t: string) => {
31
+ resolveSession: async (t: string) => {
29
32
  if (t === JWT) return { userId: 'u-jwt', email: 'jwt@x' };
33
+ if (t === JWT_DURING_OUTAGE) throw new AuthBackendError(new Error('the database is down'));
30
34
  throw new Error('bad jwt');
31
35
  },
36
+ isActive: async () => true,
32
37
  } as unknown as AuthService;
33
38
 
34
39
  let server: HttpServer | undefined;
@@ -75,6 +80,14 @@ describe('createManualAuthMiddleware (read-only manual endpoints)', () => {
75
80
  expect((await get(base, '/api/agent/utcp', 'garbage')).status).toBe(401);
76
81
  });
77
82
 
83
+ it('answers 500, not 401, when the session’s account cannot be looked up', async () => {
84
+ const { base } = await start();
85
+ const r = await get(base, '/api/agent/utcp', JWT_DURING_OUTAGE);
86
+ // A 401 would sign a valid caller out over an outage.
87
+ expect(r.status).toBe(500);
88
+ expect(r.headers.get('www-authenticate')).toBeNull();
89
+ });
90
+
78
91
  it('refuses the INTERNAL manual to a browser JWT (403) but allows an internal token', async () => {
79
92
  const { base, intToken } = await start();
80
93
  expect((await get(base, '/api/agent/internal/utcp', JWT)).status).toBe(403);
@@ -108,7 +108,9 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
108
108
  // Join users so a single round-trip resolves both "token is valid" and
109
109
  // "load the user it belongs to". The unique index on token_hash makes
110
110
  // this a point-lookup. `isNull(revokedAt)` is what enforces revocation
111
- // — the row remains for audit.
111
+ // — the row remains for audit — and `isNull(deactivatedAt)` is what
112
+ // stops a switched-off account's keys without revoking them, so
113
+ // switching it back on restores them.
112
114
  const [row] = await this.db
113
115
  .select({
114
116
  tokenId: externalApiKeys.id,
@@ -119,7 +121,7 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
119
121
  })
120
122
  .from(externalApiKeys)
121
123
  .innerJoin(users, eq(externalApiKeys.userId, users.id))
122
- .where(and(eq(externalApiKeys.tokenHash, tokenHash), isNull(externalApiKeys.revokedAt)))
124
+ .where(and(eq(externalApiKeys.tokenHash, tokenHash), isNull(externalApiKeys.revokedAt), isNull(users.deactivatedAt)))
123
125
  .limit(1);
124
126
 
125
127
  if (!row) return null;
@@ -12,9 +12,9 @@ export interface InternalTokenClaim {
12
12
  /**
13
13
  * The agent run / conversation id (the Mastra thread id) this token is scoped
14
14
  * to, when minted per-run. Surfaced as `ToolContext.sessionId` so the
15
- * ontology-session boundary can scope to one run for the in-process agent
16
- * (the external/MCP path carries `sessionId` on the tool body instead).
17
- * Absent for identity-only tokens.
15
+ * agent-access hooks can tell the in-process agent's run apart (the
16
+ * external/MCP path carries `sessionId` on the tool body instead). Absent
17
+ * for identity-only tokens.
18
18
  */
19
19
  sessionId?: string;
20
20
  /**
@@ -4,6 +4,7 @@ import { logger } from '../../shared/logging.js';
4
4
  const log = logger('tools');
5
5
  import type { IExternalApiKeyService } from './external-api-key.interface.js';
6
6
  import type { AuthService } from '../auth/auth.service.js';
7
+ import { ACCOUNT_DEACTIVATED_MESSAGE, AuthBackendError } from '../auth/account-admission.js';
7
8
  import { InternalTokenService } from './internal-token.service.js';
8
9
  import { INVALID_CONNECTION_KEY_CHALLENGE, INVALID_CONNECTION_KEY_MESSAGE } from './connection-key-rejection.js';
9
10
 
@@ -79,10 +80,10 @@ export const requireInternalSource: RequestHandler = (req, res, next) => {
79
80
 
80
81
  /**
81
82
  * Mirror of `requireInternalSource` for surfaces that must admit ONLY external
82
- * callers. `start_session` mints a fresh chat thread as the caller's
83
- * ontology-session id; an internal (in-process agent) token already carries
84
- * its run's `sessionId`, so letting it mint a new thread mid-run would
85
- * silently reset the one-ontology-per-conversation boundary.
83
+ * callers. `start_session` mints a fresh chat thread as the caller's session
84
+ * id; an internal (in-process agent) token already carries its run's
85
+ * `sessionId`, so letting it mint a new thread mid-run would silently split
86
+ * one conversation in two.
86
87
  *
87
88
  * "External" is decided by the VERIFIER, not by which credential shape
88
89
  * authenticated: a `bevel_…` connection key resolves external, and so does an
@@ -122,6 +123,15 @@ export type VerifyResult =
122
123
  export function createTokenVerifier(
123
124
  externalApiKeyService: IExternalApiKeyService,
124
125
  internalTokenService: InternalTokenService,
126
+ /**
127
+ * Whether the account an internal token was minted for is still on. An
128
+ * internal token carries only a user id and lives up to an hour (longer
129
+ * for the MCP loopback), so without this a switched-off account's running
130
+ * agent would keep working until it expired. Connection keys need no
131
+ * second look: their lookup reads the account. Every deployment passes
132
+ * it; absent, internal tokens are judged on their signature alone.
133
+ */
134
+ accounts?: Pick<AuthService, 'isActive'>,
125
135
  ): (token: string | undefined) => Promise<VerifyResult> {
126
136
  const missing = { ok: false as const, status: 401, message: 'A connection key or internal token is required' };
127
137
  return async (token) => {
@@ -136,6 +146,9 @@ export function createTokenVerifier(
136
146
  if (internalTokenService.looksLikeInternalToken(token)) {
137
147
  const claim = internalTokenService.verify(token);
138
148
  if (!claim) return { ok: false, status: 401, message: 'Invalid or expired internal token' };
149
+ if (accounts && !(await accounts.isActive(claim.userId))) {
150
+ return { ok: false, status: 401, message: ACCOUNT_DEACTIVATED_MESSAGE };
151
+ }
139
152
  return claim.externalProxy
140
153
  ? {
141
154
  ok: true,
@@ -170,8 +183,9 @@ export function createTokenVerifier(
170
183
  export function createToolAuthMiddleware(
171
184
  externalApiKeyService: IExternalApiKeyService,
172
185
  internalTokenService: InternalTokenService,
186
+ accounts?: Pick<AuthService, 'isActive'>,
173
187
  ): RequestHandler {
174
- const verify = createTokenVerifier(externalApiKeyService, internalTokenService);
188
+ const verify = createTokenVerifier(externalApiKeyService, internalTokenService, accounts);
175
189
  return async (req: Request, res: Response, next: NextFunction) => {
176
190
  const header = req.headers.authorization;
177
191
  if (!header || !header.toLowerCase().startsWith('bearer ')) {
@@ -210,9 +224,9 @@ export function createToolAuthMiddleware(
210
224
  export function createManualAuthMiddleware(
211
225
  externalApiKeyService: IExternalApiKeyService,
212
226
  internalTokenService: InternalTokenService,
213
- authService: AuthService,
227
+ authService: Pick<AuthService, 'isActive' | 'resolveSession'>,
214
228
  ): RequestHandler {
215
- const verify = createTokenVerifier(externalApiKeyService, internalTokenService);
229
+ const verify = createTokenVerifier(externalApiKeyService, internalTokenService, authService);
216
230
  return async (req: Request, res: Response, next: NextFunction) => {
217
231
  const header = req.headers.authorization;
218
232
  if (!header || !header.toLowerCase().startsWith('bearer ')) {
@@ -241,13 +255,22 @@ export function createManualAuthMiddleware(
241
255
  }
242
256
 
243
257
  // Otherwise treat the bearer as a browser session JWT.
258
+ let userId: string;
244
259
  try {
245
- const { userId } = authService.verifyToken(token);
246
- req.toolAuth = { source: 'session', userId, scope: 'write' };
247
- next();
248
- } catch {
260
+ ({ userId } = await authService.resolveSession(token));
261
+ } catch (err) {
262
+ // The account could not be looked up: an outage, not a verdict on the
263
+ // session, and a 401 would sign a valid caller out over it.
264
+ if (err instanceof AuthBackendError) {
265
+ log.error('session account lookup failed:', { err: err.cause });
266
+ res.status(500).json({ error: 'Authentication backend unavailable' });
267
+ return;
268
+ }
249
269
  res.setHeader('WWW-Authenticate', WWW_AUTH);
250
270
  res.status(401).json({ error: 'Invalid or expired token' });
271
+ return;
251
272
  }
273
+ req.toolAuth = { source: 'session', userId, scope: 'write' };
274
+ next();
252
275
  };
253
276
  }
@@ -12,6 +12,7 @@ import { createToolHandlerFactory } from '../tool-handler.js';
12
12
  import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
13
13
  import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
14
14
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
15
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
15
16
  import { SpillStore } from '../../workspace/spill-store.js';
16
17
  import { DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
17
18
  import { NEW_ROLE_GUIDANCE } from '../../access-model/roles-yaml-guard.js';
@@ -68,13 +69,14 @@ async function start(): Promise<string> {
68
69
  canOwner: async () => true,
69
70
  canWriteBatchAtRef: async () => null,
70
71
  canReadBatch: async (_w: string, _u: string, paths: string[]) => new Map(paths.map((p) => [p, true])),
72
+ // The `after` half of a move's or copy's preview — allowing everything, like the gates above.
73
+ previewAccessAfterRelocation: async () => ({ read: true, write: true, download: true, owner: true }),
71
74
  } as never;
72
75
  registerWorkspaceTools(registry, router, toolAuth, createToolHandlerFactory(resolve), new SpillStore(path.join(os.tmpdir(), 'bevel-test-spills')), new DocExtractService(docCache), allowAll, testKbContext({ kbDirName: KB }), {
73
- service: {} as never,
74
- enabled: false, // ontology boundary not under test here
75
- kb: testKbContext({ kbDirName: KB }),
76
+ // No hook registered: the agent-access gate refuses nothing here.
76
77
  recoveryBotEmail: 'recovery-bot@bevel.local',
77
78
  hooks: new WorkflowHooks(),
79
+ notes: new ToolDescriptionNotes(),
78
80
  }, new RoutineWritePolicyService(), {} as never);
79
81
  const app = express();
80
82
  app.use(express.json());
@@ -10,6 +10,7 @@ import { createManualRoutes } from '../../tool-registry/manual.routes.js';
10
10
  import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
11
11
  import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
12
12
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
13
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
13
14
  import { SpillStore } from '../../workspace/spill-store.js';
14
15
  import { DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
15
16
  import { testKbContext } from '../../../__tests__/kb-context.js';
@@ -65,11 +66,10 @@ async function start(): Promise<string> {
65
66
  // and their web_search / submit_feedback / cite_kb_node cases — moved to
66
67
  // the enterprise repo with their modules; core registers only core tools.
67
68
  registerWorkspaceTools(registry, router, toolAuth, th, new SpillStore('/tmp/bevel-test-spills'), new DocExtractService('/tmp/bevel-test-doc-extract'), accessControl, testKbContext(), {
68
- service: {} as never,
69
- enabled: false, // ontology boundary not under test here
70
- kb: testKbContext(),
69
+ // No hook registered: the agent-access gate refuses nothing here.
71
70
  recoveryBotEmail: 'recovery-bot@bevel.local',
72
71
  hooks: new WorkflowHooks(),
72
+ notes: new ToolDescriptionNotes(),
73
73
  }, new RoutineWritePolicyService(), {} as never /* sessionSink — start_session not exercised here */);
74
74
  router.use(createManualRoutes(registry, toolAuth));
75
75
 
@@ -18,7 +18,11 @@ const externalApiKeyService = {
18
18
  verifyAndLoadToken: async (t: string) =>
19
19
  t === 'bevel_ok' ? { user: { id: 'u-ext', email: 'e@x', name: 'Ext' }, tokenId: 'tk-1' } : null,
20
20
  } as never;
21
- const authService = { getUserById: async (id: string) => ({ id, email: 'e@x', name: 'N' }) } as never;
21
+ // Every account is on except `u-off`, which an admin switched off.
22
+ const authService = {
23
+ getUserById: async (id: string) => ({ id, email: 'e@x', name: 'N' }),
24
+ isActive: async (id: string) => id !== 'u-off',
25
+ } as never;
22
26
 
23
27
  let tempDir = '';
24
28
  const workspaceService = {
@@ -66,6 +70,12 @@ describe('validateToken (tool-author SDK entry)', () => {
66
70
  expect((await fs.readFile('a.md')).toString()).toBe('hi');
67
71
  });
68
72
 
73
+ it('throws a 401 ToolError for a valid internal token whose account was switched off', async () => {
74
+ const err = await validateToken(internalToken.mint({ userId: 'u-off' })).catch((e) => e);
75
+ expect(hasHttpStatus(err) && err.status).toBe(401);
76
+ expect((err as Error).message).toContain('switched off');
77
+ });
78
+
69
79
  it('throws a 401 ToolError on a bad token and an empty token', async () => {
70
80
  for (const bad of ['garbage', 'bevel_nope', '', undefined]) {
71
81
  const err = await validateToken(bad as string).catch((e) => e);
@@ -12,6 +12,7 @@ import type { ICreatorAccess } from '../access-model/creator.js';
12
12
  import type { GroupsIndex } from '../access-model/group-files.js';
13
13
  import type { WorkspaceService } from '../workspace/workspace.service.js';
14
14
  import { isAbsence } from '../../shared/fs.contract.js';
15
+ import { assertBranchProvided } from '../../shared/domain-errors.js';
15
16
  import { branchForWorkspaceId } from '../../shared/workspace-id.js';
16
17
  import type { WorkflowEventBus } from '../workflow/event-bus.js';
17
18
  import type { AuthService } from '../auth/auth.service.js';
@@ -77,6 +78,20 @@ export function createToolContextResolver(deps: ToolContextDeps): ResolveToolCon
77
78
  const { loadActiveGroups } = deps;
78
79
  const fsCache = new Map<string, LocalFilesystem>();
79
80
  const getFilesystem = async (branch: string): Promise<LocalFilesystem> => {
81
+ // THE choke point: every knowledge-base tool resolves its workspace
82
+ // through here, and a `call_tool_chain` call reaches the very same route
83
+ // over loopback — so refusing a branch-less call here refuses it on both
84
+ // paths at once, with no per-tool guard to keep in step.
85
+ //
86
+ // Before ANY workspace work, because both things downstream do with this
87
+ // value are wrong when it is absent: `getOrCreateForUser` defaults a
88
+ // missing branch to the deployment's default (protected) branch, so an
89
+ // omitted argument would silently act on `main`; and a present-but-absent
90
+ // value like the string "undefined" goes to `workspaceIdForBranch`, which
91
+ // makes it a workspace directory of that name and then tries to clone a
92
+ // branch nobody ever pushed. Failing closed here means no workspace is
93
+ // created and no clone is attempted.
94
+ assertBranchProvided(branch);
80
95
  const cached = fsCache.get(branch);
81
96
  if (cached) return cached;
82
97
  const ws = await deps.workspaceService.getOrCreateForUser(user, branch);
@@ -6,6 +6,7 @@ import { hasHttpStatus, ToolError, type ToolHandler } from './tool.contract.js';
6
6
  import { WorkflowDomainError } from '../../shared/domain-errors.js';
7
7
  import { domainErrorBody } from '../../shared/http-errors.js';
8
8
  import type { ResolveToolContext } from './tool-context.js';
9
+ import { alwaysWritable, READ_ONLY_CODE, refuseWriteTool, type IWriteAccess } from '../write-access/write-access.js';
9
10
  import '../tool-auth/tool-auth.middleware.js'; // Express Request.toolAuth augmentation
10
11
 
11
12
  function isAsyncIterable(v: unknown): v is AsyncIterable<unknown> {
@@ -29,7 +30,7 @@ export interface ToolHandlerOptions {
29
30
  * The handler receives `req.body` as the flat args (UTCP's `body_field` already
30
31
  * delivered the inner body as the request body).
31
32
  */
32
- export function createToolHandlerFactory(resolve: ResolveToolContext) {
33
+ export function createToolHandlerFactory(resolve: ResolveToolContext, writeAccess: IWriteAccess = alwaysWritable) {
33
34
  return function toolHandler(handler: ToolHandler, opts: ToolHandlerOptions = {}) {
34
35
  return async (req: Request, res: Response): Promise<void> => {
35
36
  const auth = req.toolAuth;
@@ -41,10 +42,29 @@ export function createToolHandlerFactory(resolve: ResolveToolContext) {
41
42
  res.status(403).json({ error: 'This tool requires write access.' });
42
43
  return;
43
44
  }
45
+ // Before anything is awaited: a client that goes away during the
46
+ // write-access check below must still abort the call.
47
+ //
48
+ // Asked of the RESPONSE. The request closes as soon as its body has
49
+ // been read, with the client still there and waiting, so its `close`
50
+ // says nothing about the client; the response closes when it has been
51
+ // sent or the connection is gone, and `writableEnded` tells the two
52
+ // apart.
44
53
  const abort = new AbortController();
45
- req.on('close', () => {
54
+ res.on('close', () => {
46
55
  if (!res.writableEnded) abort.abort();
47
56
  });
57
+ // The tool layer's half of the read-only gate: the HTTP gate lets every
58
+ // tool call through, since only here is a write tool told from a read.
59
+ if (opts.write) {
60
+ const refusal = await refuseWriteTool(writeAccess);
61
+ if (refusal !== null) {
62
+ res.status(403).json({ error: refusal, code: READ_ONLY_CODE });
63
+ return;
64
+ }
65
+ // The client left while the verdict was awaited: nobody to answer.
66
+ if (abort.signal.aborted) return;
67
+ }
48
68
  const body: unknown = req.body;
49
69
  if (body !== undefined && body !== null && (typeof body !== 'object' || Array.isArray(body))) {
50
70
  res.status(400).json({ error: 'Request body must be a JSON object.' });
@@ -53,8 +73,8 @@ export function createToolHandlerFactory(resolve: ResolveToolContext) {
53
73
  const args = (body ?? {}) as Record<string, unknown>;
54
74
  // `sessionId` rides the tool body like `branch` does: the external MCP
55
75
  // proxy injects it (ask-tool continuity convention) and the in-process
56
- // agent passes its thread id. Surfaced on the context so the ontology
57
- // gate can scope to one run without each handler re-reading args.
76
+ // agent passes its thread id. Surfaced on the context so the
77
+ // agent-access gate can name the run without each handler re-reading args.
58
78
  const sessionId = typeof args.sessionId === 'string' && args.sessionId.length > 0
59
79
  ? args.sessionId
60
80
  : undefined;
@@ -34,8 +34,9 @@ export interface ToolContext {
34
34
  * The agent run / conversation id this call belongs to (the agent thread id).
35
35
  * Supplied as a `sessionId` field on the tool's input body — the external MCP
36
36
  * proxy injects it via the `ask`-tool continuity convention, and the
37
- * in-process agent passes its thread id. Used to scope the ontology-session
38
- * boundary to one run. Absent for non-agent or unthreaded calls.
37
+ * in-process agent passes its thread id. Handed to the agent-access hooks,
38
+ * which is how a deployment scopes a rule to one conversation. Absent for
39
+ * non-agent or unthreaded calls.
39
40
  */
40
41
  sessionId?: string;
41
42
  /**
@@ -35,7 +35,7 @@ export type ValidateToken = (
35
35
  ) => Promise<ToolContext>;
36
36
 
37
37
  export function createToolValidator(deps: ToolValidatorDeps): ValidateToken {
38
- const verify = createTokenVerifier(deps.externalApiKeyService, deps.internalTokenService);
38
+ const verify = createTokenVerifier(deps.externalApiKeyService, deps.internalTokenService, deps.authService);
39
39
  const resolve = createToolContextResolver(deps);
40
40
  return async (token, signal) => {
41
41
  let result;
@@ -582,7 +582,7 @@ describe('WorkflowService — a refusal clears when a change makes it obsolete',
582
582
  const { svc } = service(
583
583
  store.db,
584
584
  (e) => emitted.push(e as { kind: string }),
585
- {},
585
+ { carryApprovalsForward: vi.fn(async () => 0) },
586
586
  {
587
587
  git: {
588
588
  pull: vi.fn(async () => ({ treeChanged: false })),
@@ -591,6 +591,14 @@ describe('WorkflowService — a refusal clears when a change makes it obsolete',
591
591
  return { kind: 'merged', alreadyUpToDate: false };
592
592
  }),
593
593
  push: vi.fn(async () => undefined),
594
+ hasUnpushedCommits: vi.fn(async () => true),
595
+ pathsChangedBetween: vi.fn(async () => []),
596
+ // The head this test is named for: one sha before the merge,
597
+ // another after it.
598
+ resolvePrShas: vi
599
+ .fn()
600
+ .mockResolvedValueOnce({ baseSha: 'b'.repeat(40), headSha: 'a'.repeat(40) })
601
+ .mockResolvedValue({ baseSha: 'b'.repeat(40), headSha: 'd'.repeat(40) }),
594
602
  } as unknown as Partial<GitService>,
595
603
  prs: {
596
604
  getPrDetail: vi.fn(async () => ({ state: 'open', branch: 'bo/suggestions', base: 'main', viewerCanUpdate: true })),