@bevel-software/platform-core-backend 0.26.0 → 0.27.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 (613) hide show
  1. package/agent-guide/access-control.md +17 -0
  2. package/agent-guide/conventions.md +8 -0
  3. package/agent-guide/html-views.md +83 -0
  4. package/agent-guide/new-knowledge-base.md +24 -0
  5. package/agent-guide/tool-manuals.md +10 -0
  6. package/dist/assets.d.ts +19 -0
  7. package/dist/assets.d.ts.map +1 -1
  8. package/dist/assets.js +26 -2
  9. package/dist/assets.js.map +1 -1
  10. package/dist/core/catalog-revision.d.ts +19 -1
  11. package/dist/core/catalog-revision.d.ts.map +1 -1
  12. package/dist/core/catalog-revision.js +14 -3
  13. package/dist/core/catalog-revision.js.map +1 -1
  14. package/dist/core/create-core-server.d.ts +5 -0
  15. package/dist/core/create-core-server.d.ts.map +1 -1
  16. package/dist/core/create-core-server.js +54 -3
  17. package/dist/core/create-core-server.js.map +1 -1
  18. package/dist/core/create-core-services.d.ts +26 -0
  19. package/dist/core/create-core-services.d.ts.map +1 -1
  20. package/dist/core/create-core-services.js +100 -8
  21. package/dist/core/create-core-services.js.map +1 -1
  22. package/dist/core/lifecycle.d.ts +16 -1
  23. package/dist/core/lifecycle.d.ts.map +1 -1
  24. package/dist/core/lifecycle.js +18 -1
  25. package/dist/core/lifecycle.js.map +1 -1
  26. package/dist/core-config.d.ts +21 -0
  27. package/dist/core-config.d.ts.map +1 -1
  28. package/dist/core-config.js +22 -1
  29. package/dist/core-config.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/modules/access/access-control.interface.d.ts +8 -0
  35. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  36. package/dist/modules/access/access-control.service.d.ts +1 -0
  37. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.js +11 -0
  39. package/dist/modules/access/access-control.service.js.map +1 -1
  40. package/dist/modules/access/directory-sync-bot.js +1 -1
  41. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  42. package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
  43. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  44. package/dist/modules/agent-guide/agent-guide.js +8 -0
  45. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  46. package/dist/modules/agent-guide/index.d.ts +1 -1
  47. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  48. package/dist/modules/agent-guide/index.js +1 -1
  49. package/dist/modules/agent-guide/index.js.map +1 -1
  50. package/dist/modules/agent-instructions/compose.d.ts +5 -0
  51. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  52. package/dist/modules/agent-instructions/compose.js +19 -2
  53. package/dist/modules/agent-instructions/compose.js.map +1 -1
  54. package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
  55. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  56. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  57. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  58. package/dist/modules/audit/agent-audit.service.js +43 -2
  59. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  60. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  61. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  62. package/dist/modules/audit/agent-connection.routes.js +38 -0
  63. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  64. package/dist/modules/audit/audit.contract.d.ts +24 -0
  65. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  66. package/dist/modules/audit/audit.contract.js.map +1 -1
  67. package/dist/modules/audit/index.d.ts +1 -0
  68. package/dist/modules/audit/index.d.ts.map +1 -1
  69. package/dist/modules/audit/index.js +1 -0
  70. package/dist/modules/audit/index.js.map +1 -1
  71. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  72. package/dist/modules/auth/account.routes.js +10 -3
  73. package/dist/modules/auth/account.routes.js.map +1 -1
  74. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  75. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  76. package/dist/modules/auth/auth.middleware.js +1 -1
  77. package/dist/modules/auth/auth.middleware.js.map +1 -1
  78. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  79. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  80. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  81. package/dist/modules/database/core-schema.d.ts +96 -0
  82. package/dist/modules/database/core-schema.d.ts.map +1 -1
  83. package/dist/modules/database/core-schema.js +32 -3
  84. package/dist/modules/database/core-schema.js.map +1 -1
  85. package/dist/modules/embed/account-link.service.d.ts +42 -0
  86. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  87. package/dist/modules/embed/account-link.service.js +50 -0
  88. package/dist/modules/embed/account-link.service.js.map +1 -0
  89. package/dist/modules/embed/embed-link.d.ts +58 -0
  90. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  91. package/dist/modules/embed/embed-link.js +160 -0
  92. package/dist/modules/embed/embed-link.js.map +1 -0
  93. package/dist/modules/embed/embed.errors.d.ts +30 -0
  94. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  95. package/dist/modules/embed/embed.errors.js +42 -0
  96. package/dist/modules/embed/embed.errors.js.map +1 -0
  97. package/dist/modules/embed/embed.interface.d.ts +204 -0
  98. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  99. package/dist/modules/embed/embed.interface.js +2 -0
  100. package/dist/modules/embed/embed.interface.js.map +1 -0
  101. package/dist/modules/embed/embed.routes.d.ts +59 -0
  102. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  103. package/dist/modules/embed/embed.routes.js +343 -0
  104. package/dist/modules/embed/embed.routes.js.map +1 -0
  105. package/dist/modules/embed/embed.service.d.ts +169 -0
  106. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  107. package/dist/modules/embed/embed.service.js +627 -0
  108. package/dist/modules/embed/embed.service.js.map +1 -0
  109. package/dist/modules/embed/embed.tools.d.ts +51 -0
  110. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  111. package/dist/modules/embed/embed.tools.js +170 -0
  112. package/dist/modules/embed/embed.tools.js.map +1 -0
  113. package/dist/modules/embed/mcp-app.d.ts +64 -0
  114. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  115. package/dist/modules/embed/mcp-app.js +147 -0
  116. package/dist/modules/embed/mcp-app.js.map +1 -0
  117. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  118. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  119. package/dist/modules/embed/mcp-app.routes.js +36 -0
  120. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  121. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  122. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  123. package/dist/modules/github-app/github-app.connection.js +34 -3
  124. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  125. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  126. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  127. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  128. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  129. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  130. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  131. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  132. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  133. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  134. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  135. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  136. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  137. package/dist/modules/mcp/mcp.service.d.ts +28 -1
  138. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  139. package/dist/modules/mcp/mcp.service.js +64 -8
  140. package/dist/modules/mcp/mcp.service.js.map +1 -1
  141. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  142. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  143. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  144. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  145. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  146. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  147. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  148. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  149. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  150. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  151. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  152. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  153. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  154. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  155. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  156. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  157. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  158. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  159. package/dist/modules/onboarding/starter-packs.js +156 -0
  160. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  161. package/dist/modules/plugins/index.d.ts +1 -1
  162. package/dist/modules/plugins/index.d.ts.map +1 -1
  163. package/dist/modules/plugins/index.js +1 -1
  164. package/dist/modules/plugins/index.js.map +1 -1
  165. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  166. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  167. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  168. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  169. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  170. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  171. package/dist/modules/plugins/plugins.routes.js +31 -0
  172. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  173. package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
  174. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  175. package/dist/modules/settings/deployment-settings.service.js +134 -1
  176. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  177. package/dist/modules/settings/repository-source.d.ts +1 -0
  178. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  179. package/dist/modules/settings/repository-source.js +3 -3
  180. package/dist/modules/settings/repository-source.js.map +1 -1
  181. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  182. package/dist/modules/skills/skills.tools.js +50 -32
  183. package/dist/modules/skills/skills.tools.js.map +1 -1
  184. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  185. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  186. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  187. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  188. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  189. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  190. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  191. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  192. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  193. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  194. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  195. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  196. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  197. package/dist/modules/tool-helpers/index.d.ts +28 -1
  198. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  199. package/dist/modules/tool-helpers/index.js +28 -1
  200. package/dist/modules/tool-helpers/index.js.map +1 -1
  201. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  202. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  203. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  204. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  205. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  206. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  207. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  208. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  209. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  210. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  211. package/dist/modules/tool-helpers/tool-def.js +70 -6
  212. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  213. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  214. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  215. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  216. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  217. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  218. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  219. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  220. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  221. package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
  222. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  223. package/dist/modules/tool-registry/description-length.d.ts +6 -2
  224. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  225. package/dist/modules/tool-registry/description-length.js +27 -5
  226. package/dist/modules/tool-registry/description-length.js.map +1 -1
  227. package/dist/modules/tool-registry/guide-first.d.ts +4 -0
  228. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
  229. package/dist/modules/tool-registry/guide-first.js +9 -1
  230. package/dist/modules/tool-registry/guide-first.js.map +1 -1
  231. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
  232. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  233. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
  234. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  235. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  236. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  237. package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
  238. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  239. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  240. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  241. package/dist/modules/workflow/file-lock.service.js +62 -5
  242. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  243. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  244. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  245. package/dist/modules/workflow/git/git.service.js +172 -36
  246. package/dist/modules/workflow/git/git.service.js.map +1 -1
  247. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  248. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  249. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  250. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  251. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  252. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  253. package/dist/modules/workflow/pending-commits.service.js +25 -1
  254. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  255. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  256. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  257. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  258. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  259. package/dist/modules/workflow/recovery-bot.js +1 -1
  260. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  261. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
  263. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  264. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  265. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  266. package/dist/modules/workflow/sanitize-error.js +45 -0
  267. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  268. package/dist/modules/workflow/workflow.service.d.ts +183 -15
  269. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  270. package/dist/modules/workflow/workflow.service.js +805 -106
  271. package/dist/modules/workflow/workflow.service.js.map +1 -1
  272. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  273. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  274. package/dist/modules/workspace/agent-download.builder.js +238 -0
  275. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  276. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  277. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  278. package/dist/modules/workspace/agent-download.routes.js +156 -0
  279. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  280. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  281. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  282. package/dist/modules/workspace/agent-download.store.js +423 -0
  283. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  284. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  285. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  286. package/dist/modules/workspace/agent-upload.store.js +16 -6
  287. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  288. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  289. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  290. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  291. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  292. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  293. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  294. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  295. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  296. package/dist/modules/workspace/first-run.d.ts +111 -0
  297. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  298. package/dist/modules/workspace/first-run.js +181 -0
  299. package/dist/modules/workspace/first-run.js.map +1 -0
  300. package/dist/modules/workspace/move-links.d.ts +92 -0
  301. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  302. package/dist/modules/workspace/move-links.js +173 -0
  303. package/dist/modules/workspace/move-links.js.map +1 -0
  304. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  305. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
  306. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  307. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  308. package/dist/modules/workspace/workspace.routes.js +63 -22
  309. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  310. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  311. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  312. package/dist/modules/workspace/workspace.service.js +197 -24
  313. package/dist/modules/workspace/workspace.service.js.map +1 -1
  314. package/dist/modules/workspace/workspace.tools.d.ts +59 -1
  315. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  316. package/dist/modules/workspace/workspace.tools.js +443 -65
  317. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  318. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  319. package/dist/modules/write-access/write-access.js +9 -0
  320. package/dist/modules/write-access/write-access.js.map +1 -1
  321. package/dist/shared/domain-errors.d.ts +73 -1
  322. package/dist/shared/domain-errors.d.ts.map +1 -1
  323. package/dist/shared/domain-errors.js +86 -12
  324. package/dist/shared/domain-errors.js.map +1 -1
  325. package/dist/shared/git.contract.d.ts +10 -1
  326. package/dist/shared/git.contract.d.ts.map +1 -1
  327. package/dist/shared/git.contract.js.map +1 -1
  328. package/dist/shared/utf8-text.d.ts +15 -0
  329. package/dist/shared/utf8-text.d.ts.map +1 -0
  330. package/dist/shared/utf8-text.js +20 -0
  331. package/dist/shared/utf8-text.js.map +1 -0
  332. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  333. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  334. package/dist/tenancy/static-tenant-source.js +9 -2
  335. package/dist/tenancy/static-tenant-source.js.map +1 -1
  336. package/mcp-app/page.html +349 -0
  337. package/migrations/0017_atlassian_account_links.sql +37 -0
  338. package/migrations/meta/0017_snapshot.json +2396 -0
  339. package/migrations/meta/_journal.json +8 -1
  340. package/package.json +5 -3
  341. package/src/__tests__/packaged-assets-ship.test.ts +16 -4
  342. package/src/assets.ts +28 -2
  343. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  344. package/src/core/__tests__/gated-boot.test.ts +1 -0
  345. package/src/core/__tests__/lifecycle.test.ts +14 -0
  346. package/src/core/catalog-revision.ts +15 -2
  347. package/src/core/create-core-server.ts +67 -3
  348. package/src/core/create-core-services.ts +140 -6
  349. package/src/core/lifecycle.ts +28 -2
  350. package/src/core-config.ts +24 -1
  351. package/src/index.ts +2 -2
  352. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  353. package/src/modules/access/access-control.interface.ts +13 -0
  354. package/src/modules/access/access-control.service.ts +16 -0
  355. package/src/modules/access/directory-sync-bot.ts +1 -1
  356. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
  357. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
  358. package/src/modules/agent-guide/agent-guide.ts +9 -0
  359. package/src/modules/agent-guide/index.ts +1 -0
  360. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
  361. package/src/modules/agent-instructions/compose.ts +17 -2
  362. package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
  363. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  364. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  365. package/src/modules/audit/agent-audit.service.ts +58 -3
  366. package/src/modules/audit/agent-connection.routes.ts +56 -0
  367. package/src/modules/audit/audit.contract.ts +26 -0
  368. package/src/modules/audit/index.ts +1 -0
  369. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  370. package/src/modules/auth/account.routes.ts +10 -3
  371. package/src/modules/auth/auth.middleware.ts +1 -1
  372. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  373. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  374. package/src/modules/database/core-schema.ts +33 -3
  375. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  376. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  377. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  378. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  379. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  380. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  381. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  382. package/src/modules/embed/account-link.service.ts +76 -0
  383. package/src/modules/embed/embed-link.ts +180 -0
  384. package/src/modules/embed/embed.errors.ts +43 -0
  385. package/src/modules/embed/embed.interface.ts +203 -0
  386. package/src/modules/embed/embed.routes.ts +363 -0
  387. package/src/modules/embed/embed.service.ts +705 -0
  388. package/src/modules/embed/embed.tools.ts +206 -0
  389. package/src/modules/embed/mcp-app.routes.ts +41 -0
  390. package/src/modules/embed/mcp-app.ts +173 -0
  391. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  392. package/src/modules/github-app/github-app.connection.ts +38 -4
  393. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  394. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  395. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  396. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  397. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  398. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  399. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  400. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  401. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  402. package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
  403. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  404. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  405. package/src/modules/mcp/mcp.service.ts +81 -12
  406. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  407. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  408. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  409. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  410. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  411. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  412. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  413. package/src/modules/onboarding/starter-packs.ts +182 -0
  414. package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
  415. package/src/modules/plugins/index.ts +1 -0
  416. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  417. package/src/modules/plugins/plugins.routes.ts +29 -0
  418. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
  419. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  420. package/src/modules/settings/deployment-settings.service.ts +131 -1
  421. package/src/modules/settings/repository-source.ts +4 -4
  422. package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
  423. package/src/modules/skills/skills.tools.ts +55 -35
  424. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  425. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  426. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  427. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  428. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  429. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  430. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  431. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  432. package/src/modules/tool-helpers/index.ts +34 -1
  433. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  434. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  435. package/src/modules/tool-helpers/tool-def.ts +132 -7
  436. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  437. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  438. package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
  439. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
  440. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
  441. package/src/modules/tool-registry/description-length.ts +34 -7
  442. package/src/modules/tool-registry/guide-first.ts +8 -1
  443. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  444. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  445. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  446. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  447. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  448. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  449. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  450. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  451. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  452. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  453. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  454. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  455. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
  457. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
  458. package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
  459. package/src/modules/workflow/file-lock.service.ts +67 -5
  460. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  461. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  462. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  463. package/src/modules/workflow/git/git.service.ts +176 -38
  464. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  465. package/src/modules/workflow/pending-commits.service.ts +29 -1
  466. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  467. package/src/modules/workflow/recovery-bot.ts +1 -1
  468. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
  469. package/src/modules/workflow/sanitize-error.ts +50 -0
  470. package/src/modules/workflow/workflow.service.ts +879 -112
  471. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  472. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  473. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  474. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  475. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  476. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  477. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  478. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  479. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  480. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  481. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  482. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  483. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  484. package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
  485. package/src/modules/workspace/agent-download.builder.ts +308 -0
  486. package/src/modules/workspace/agent-download.routes.ts +183 -0
  487. package/src/modules/workspace/agent-download.store.ts +533 -0
  488. package/src/modules/workspace/agent-upload.store.ts +23 -7
  489. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  490. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  491. package/src/modules/workspace/first-run.ts +222 -0
  492. package/src/modules/workspace/move-links.ts +237 -0
  493. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
  494. package/src/modules/workspace/workspace.routes.ts +64 -22
  495. package/src/modules/workspace/workspace.service.ts +202 -25
  496. package/src/modules/workspace/workspace.tools.ts +492 -63
  497. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  498. package/src/modules/write-access/write-access.ts +9 -0
  499. package/src/shared/domain-errors.ts +108 -11
  500. package/src/shared/git.contract.ts +8 -1
  501. package/src/shared/utf8-text.ts +21 -0
  502. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  503. package/src/tenancy/static-tenant-source.ts +10 -2
  504. package/starter-packs/README.md +103 -0
  505. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  506. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  507. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  508. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  509. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  510. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  511. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  512. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  513. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  514. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  515. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  516. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  517. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  518. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  519. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  520. package/starter-packs/engineering/pack.yaml +11 -0
  521. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  522. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  523. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  524. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  525. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  526. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  527. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  528. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  529. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  530. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  531. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  532. package/starter-packs/general/pack.yaml +10 -0
  533. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  534. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  535. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  536. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  537. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  538. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  539. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  540. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  541. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  542. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  543. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  544. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  545. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  546. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  547. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  548. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  549. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  550. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  551. package/starter-packs/operations/pack.yaml +12 -0
  552. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  553. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  554. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  555. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  556. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  557. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  558. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  559. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  560. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  561. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  562. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  563. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  564. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  565. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  566. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  567. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  568. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  569. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  570. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  571. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  572. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  573. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  574. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  575. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  576. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  577. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  578. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  579. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  580. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  581. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  582. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  583. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  584. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  585. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  586. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  587. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  588. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  589. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  590. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  591. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  592. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  593. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  594. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  595. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  596. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  597. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  598. package/starter-packs/sales/pack.yaml +13 -0
  599. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  600. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  601. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  602. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  603. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  604. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  605. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  606. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  607. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  608. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  609. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  610. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  611. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  612. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  613. package/starter-packs/support/pack.yaml +12 -0
@@ -1,4 +1,4 @@
1
- import { eq, inArray } from 'drizzle-orm';
1
+ import { and, eq, inArray } from 'drizzle-orm';
2
2
  import { logger } from '../../shared/logging.js';
3
3
 
4
4
  const log = logger('settings');
@@ -109,6 +109,16 @@ export const validateHttpsRemote = (value: string): string | null => {
109
109
  /**
110
110
  * The core catalogue. Order is the order the setup screen renders them in.
111
111
  */
112
+ /**
113
+ * What the `retireMergedBranches` value in effect means: on, unless it says
114
+ * off. The page saves only `true` or `false`; the environment variable is
115
+ * typed by a person, so `0`, `off` and `no` (any case) mean off too, and
116
+ * anything else — unset included — leaves the cleanup on.
117
+ */
118
+ export function retireMergedBranchesOn(raw: string): boolean {
119
+ return !/^(false|0|off|no)$/i.test(raw.trim());
120
+ }
121
+
112
122
  export const CORE_SETTINGS: SettingDef[] = [
113
123
  {
114
124
  /**
@@ -289,6 +299,23 @@ export const CORE_SETTINGS: SettingDef[] = [
289
299
  restartToApply: true,
290
300
  },
291
301
 
302
+ {
303
+ /**
304
+ * Whether the server removes, on its own, the branches merged change
305
+ * requests left behind (`WorkflowService.retireLeftoverMergedBranches`).
306
+ * On unless set to `false`; a blanked field puts the default back. Read
307
+ * at every round, so it applies without a restart. It covers that cleanup
308
+ * only: the removal of a branch when its change request is merged does
309
+ * not ask it.
310
+ */
311
+ key: 'retireMergedBranches',
312
+ envVar: 'RETIRE_MERGED_BRANCHES',
313
+ section: 'knowledge-base',
314
+ blankMeansDefault: true,
315
+ unsetMeans: 'true',
316
+ validate: (v) => (v === 'true' || v === 'false' ? null : 'Either true or false.'),
317
+ },
318
+
292
319
  {
293
320
  /**
294
321
  * The credential a git host's webhook or a pipeline presents to
@@ -372,6 +399,35 @@ export const CORE_SETTINGS: SettingDef[] = [
372
399
  validate: (v) =>
373
400
  parseRetentionWindow(v) === null ? 'Enter a whole number of days, or 0 to keep events forever.' : null,
374
401
  },
402
+
403
+ {
404
+ /**
405
+ * Which starter pack the first admin chose for a new knowledge base — a
406
+ * pack's id, or `none` for "I'll start from scratch" — written once, by
407
+ * the deployment, when the choice landed (see
408
+ * `modules/onboarding/starter-pack.service.ts`). Its presence is what
409
+ * retires the "What does your team do?" card for good; its value is what
410
+ * the first-page prompt and the agent's first-run note follow. Internal:
411
+ * a fact about what happened, not something the setup screen offers.
412
+ */
413
+ key: 'starterPack',
414
+ section: 'knowledge-base',
415
+ internal: true,
416
+ validate: (v) => (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v) ? null : 'A starter pack id is lowercase letters, digits and hyphens.'),
417
+ },
418
+ {
419
+ /**
420
+ * The claim an admin holds while their starter-pack choice is being
421
+ * written: `<user id> <epoch ms>`, inserted only if absent so two replicas
422
+ * cannot both write, and released once the choice is recorded. A claim
423
+ * nobody released in time is a process that died mid-write and is taken
424
+ * over (see `starter-pack.service.ts`). Internal, like the choice.
425
+ */
426
+ key: 'starterPackClaim',
427
+ section: 'knowledge-base',
428
+ internal: true,
429
+ validate: (v) => (/^\S+ \d+$/.test(v) ? null : 'A claim is a user id and a time.'),
430
+ },
375
431
  ];
376
432
 
377
433
  /**
@@ -724,6 +780,61 @@ export class DeploymentSettingsService {
724
780
  await this.save(entries, updatedBy, 'deployment');
725
781
  }
726
782
 
783
+ /**
784
+ * {@link record} one value ONLY IF NOTHING IS THERE, and say whether it was
785
+ * this call that put it there. The row is the claim for a flow that must
786
+ * happen once across every replica (the starter-pack choice): the insert
787
+ * yields to a row already there, so of two replicas claiming at once the
788
+ * database lets exactly one through. Validated as `record` is; a blank
789
+ * value is no claim (it would be a clear) and answers false.
790
+ */
791
+ async recordIfAbsent(key: string, value: string, updatedBy: string | null): Promise<boolean> {
792
+ const write = this.plan({ [key]: value }, 'deployment').toWrite.find((w) => w.key === key);
793
+ if (!write) return false;
794
+ const encrypted = write.def.secret === true;
795
+ const stored = encrypted ? this.crypto!.encrypt(write.value) : write.value;
796
+ const inserted = await this.db
797
+ .insert(deploymentSettings)
798
+ .values({ key, value: stored, encrypted, updatedBy })
799
+ .onConflictDoNothing({ target: deploymentSettings.key })
800
+ .returning({ key: deploymentSettings.key });
801
+ if (inserted.length === 0) return false;
802
+ this.stored.set(key, write.value);
803
+ return true;
804
+ }
805
+
806
+ /**
807
+ * Replace a plain setting's value ONLY IF it still reads `expected` —
808
+ * with `next`, or with nothing when `next` is null — and say whether it
809
+ * was this call that did it. One statement, so of two replicas acting on
810
+ * what they both read, the database lets exactly one through: the other
811
+ * finds the value gone or changed and answers false. For the claim flows
812
+ * that {@link recordIfAbsent} opens: taking over a claim that expired, and
813
+ * releasing one's own without touching a successor's. Plain settings only;
814
+ * `next` is validated as {@link record} would.
815
+ */
816
+ async swapIfValue(key: string, expected: string, next: string | null, updatedBy: string | null): Promise<boolean> {
817
+ const def = this.defs.get(key);
818
+ if (!def || def.secret) return false;
819
+ const same = and(eq(deploymentSettings.key, key), eq(deploymentSettings.value, expected));
820
+ if (next === null) {
821
+ const gone = await this.db.delete(deploymentSettings).where(same).returning({ key: deploymentSettings.key });
822
+ if (gone.length === 0) return false;
823
+ this.stored.delete(key);
824
+ return true;
825
+ }
826
+ const write = this.plan({ [key]: next }, 'deployment').toWrite.find((w) => w.key === key);
827
+ if (!write) return false;
828
+ const swapped = await this.db
829
+ .update(deploymentSettings)
830
+ .set({ value: write.value, updatedBy, updatedAt: new Date() })
831
+ .where(same)
832
+ .returning({ key: deploymentSettings.key });
833
+ if (swapped.length === 0) return false;
834
+ this.stored.set(key, write.value);
835
+ return true;
836
+ }
837
+
727
838
  /** Validate a batch and return the writes (and the clears) it amounts to; throws on any problem. */
728
839
  private plan(
729
840
  entries: Record<string, string>,
@@ -952,6 +1063,25 @@ export class DeploymentSettingsService {
952
1063
  return `${OIDC_VERIFICATION_PREFIX}${fingerprint}`;
953
1064
  }
954
1065
 
1066
+ /**
1067
+ * One plain (never sealed) setting as the DATABASE has it now, taken into
1068
+ * this replica's cache on the way: for the rare setting a running
1069
+ * deployment writes, so a replica that did not serve the write still
1070
+ * answers with it. The environment still wins, as in {@link resolve}.
1071
+ */
1072
+ async reload(key: string): Promise<string> {
1073
+ const def = this.defs.get(key);
1074
+ if (!def || def.secret) return this.resolve(key);
1075
+ const rows = await this.db
1076
+ .select({ value: deploymentSettings.value, encrypted: deploymentSettings.encrypted })
1077
+ .from(deploymentSettings)
1078
+ .where(eq(deploymentSettings.key, key));
1079
+ const row = rows.find((r) => !r.encrypted);
1080
+ if (row) this.stored.set(key, row.value);
1081
+ else this.stored.delete(key);
1082
+ return this.resolve(key);
1083
+ }
1084
+
955
1085
  /** Remove one stored row (used by tests and by `prune`). */
956
1086
  async clear(key: string): Promise<void> {
957
1087
  await this.db.delete(deploymentSettings).where(eq(deploymentSettings.key, key));
@@ -34,7 +34,7 @@ export interface GitHubAppRepository {
34
34
  * Make sure {@link token} answers with one that is still good. `asked`
35
35
  * is for a caller waiting on the answer itself, who is told a failure.
36
36
  */
37
- prepare(opts?: { asked?: boolean }): Promise<void>;
37
+ prepare(opts?: { asked?: boolean; refused?: boolean }): Promise<void>;
38
38
  /** Whether the deployment may be pointed at this repository (`owner/name`). */
39
39
  permits(repository: string, read: SettingReader): boolean;
40
40
  /** Whether the app, its installation and a repository are all there. */
@@ -98,7 +98,7 @@ export class RepositorySource {
98
98
  this.credentials = {
99
99
  username: () => this.username(),
100
100
  token: () => this.token(),
101
- prepare: () => this.prepare(),
101
+ prepare: (opts) => this.prepare(opts),
102
102
  };
103
103
  }
104
104
 
@@ -185,8 +185,8 @@ export class RepositorySource {
185
185
  }
186
186
  }
187
187
 
188
- private async prepare(): Promise<void> {
189
- if (this.inEffect === 'github-app') await this.opts.githubApp?.prepare();
188
+ private async prepare(opts?: { refused?: boolean }): Promise<void> {
189
+ if (this.inEffect === 'github-app') await this.opts.githubApp?.prepare(opts);
190
190
  }
191
191
 
192
192
  private tokenIn(read: SettingReader): string {
@@ -1,3 +1,4 @@
1
+ import { callLine } from '@bevel-software/platform-mcp-core';
1
2
  import express from 'express';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import { ToolRegistry } from '../../tool-registry/tool-registry.js';
@@ -46,7 +47,10 @@ describe('the skill tools name the available skills within the description cap',
46
47
  // than guessed, so the names below are sized to fill it EXACTLY.
47
48
  const probe = await served([skill('x')]);
48
49
  const fixed = probe.list.description!.slice(GUIDE_FIRST_SENTENCE.length + 1, probe.list.description!.indexOf('Currently available skills: '));
49
- const budget = TOOL_DESCRIPTION_CAP - GUIDE_FIRST_SENTENCE.length - 1 - fixed.length;
50
+ // And the `Call:` line a client is handed ahead of all of it, with the
51
+ // blank line after it: the example counts toward the cap too.
52
+ const call = callLine(`KNOWLEDGE_BASE.list_skills`, probe.list.inputs).length + 2;
53
+ const budget = TOOL_DESCRIPTION_CAP - call - GUIDE_FIRST_SENTENCE.length - 1 - fixed.length;
50
54
  const head = 'Currently available skills: '.length;
51
55
  // Twenty-one names, the LAST one short — shorter than the ", and 1 more…"
52
56
  // tail a cut before it would carry — and the first one sized so the
@@ -1,11 +1,14 @@
1
1
  import type { Router, RequestHandler } from 'express';
2
- import type { IToolRegistry, UtcpTool } from '../tool-registry/tool.contract.js';
2
+ import { callLine } from '@bevel-software/platform-mcp-core';
3
+ import type { IToolRegistry, JsonSchema, UtcpTool } from '../tool-registry/tool.contract.js';
3
4
  import { TOOL_DESCRIPTION_CAP } from '../tool-registry/description-length.js';
4
5
  import { GUIDE_FIRST_SENTENCE } from '../tool-registry/guide-first.js';
5
6
  import { ToolError, type ToolContext } from '../tool-helpers/tool.contract.js';
6
7
  import { toolDef } from '../tool-helpers/tool-def.js';
8
+ import { bodyEnvelope, declareRouteTool } from '../tool-helpers/route-tool-schemas.js';
7
9
  import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
8
10
  import type { ISkillService } from './skills.contract.js';
11
+ import { EXTERNAL_KB_MANUAL_NAME } from '../tool-manuals/tool-manuals.contract.js';
9
12
  import type { IAllowedToolsChecker } from './allowed-tools-check.js';
10
13
 
11
14
  /**
@@ -22,6 +25,39 @@ const BRANCH_SKILLS_INPUT = {
22
25
  'a branch that does not exist answers 404 naming it. Omit it for the approved skills.',
23
26
  };
24
27
 
28
+ /** What `list_skills` takes. A const so the route's check and the def share one declaration. */
29
+ const LIST_SKILLS_INPUTS = {
30
+ type: 'object' as const,
31
+ properties: { branch: BRANCH_SKILLS_INPUT },
32
+ additionalProperties: false,
33
+ };
34
+
35
+ /** What `get_skill` takes. A const for the same reason as {@link LIST_SKILLS_INPUTS}. */
36
+ const GET_SKILL_INPUTS = {
37
+ type: 'object' as const,
38
+ properties: {
39
+ name: { type: 'string' as const, minLength: 1, description: 'Skill name (its folder name, e.g. `rfi`).' },
40
+ file: {
41
+ type: 'string' as const,
42
+ description:
43
+ 'Optional bundled file path relative to the skill folder (e.g. `scripts/build_xlsx.py`) — ' +
44
+ 'fetch its content instead of the body.',
45
+ },
46
+ version: {
47
+ type: 'string' as const,
48
+ description:
49
+ 'Optional: the declared version to load (e.g. `1.4.0` — the `version` that `list_skills` ' +
50
+ 'reports: `metadata.version`, else `version`, else `lifecycle.version`). Omitted, the skill is loaded as it ' +
51
+ 'is now, which is the latest. Given, the skill — or the `file` — is served as it was at the most ' +
52
+ 'recent commit that declared that version; a version the skill never declared answers ' +
53
+ '`version_not_found` with the versions it did declare.',
54
+ },
55
+ branch: BRANCH_SKILLS_INPUT,
56
+ },
57
+ required: ['name'],
58
+ additionalProperties: false,
59
+ };
60
+
25
61
  /**
26
62
  * Registers the two skill tools (both surfaces) and hosts their endpoints.
27
63
  *
@@ -51,6 +87,11 @@ export function registerSkillsTools(
51
87
  registry.registerInternalTool((ctx) => buildListSkillsDef(skillService, ctx.userEmail));
52
88
  registry.registerExternalTool((ctx) => buildGetSkillDef(skillService, ctx.userEmail));
53
89
  registry.registerInternalTool((ctx) => buildGetSkillDef(skillService, ctx.userEmail));
90
+ // Both defs are built per catalog listing (their descriptions name the skills
91
+ // THIS caller may read), so declare the arguments here as well: a direct REST
92
+ // call that lands before the first listing must be checked against them too.
93
+ declareRouteTool('list_skills', LIST_SKILLS_INPUTS);
94
+ declareRouteTool('get_skill', GET_SKILL_INPUTS);
54
95
 
55
96
  router.post(
56
97
  '/agent/tools/list_skills',
@@ -139,9 +180,15 @@ async function availableSkillsLine(skillService: ISkillService, userEmail: strin
139
180
  return `${head}${names.slice(0, shown).join(', ')}${rest(shown)}`;
140
181
  }
141
182
 
142
- /** What the fixed part of a description leaves the skills line, with the guide-first sentence counted. */
143
- function skillsLineBudget(fixed: string): number {
144
- return TOOL_DESCRIPTION_CAP - GUIDE_FIRST_SENTENCE.length - 1 - fixed.length;
183
+ /**
184
+ * What the fixed part of a description leaves the skills line, with the
185
+ * `Call:` line ahead of everything (and the blank line after it) and the
186
+ * guide-first sentence counted. The call line is generated from the schema
187
+ * the tool advertises: its flat `inputs` inside the `body` envelope.
188
+ */
189
+ function skillsLineBudget(name: string, inputs: JsonSchema, fixed: string): number {
190
+ const call = callLine(`${EXTERNAL_KB_MANUAL_NAME}.${name}`, bodyEnvelope(inputs)).length + 2;
191
+ return TOOL_DESCRIPTION_CAP - call - GUIDE_FIRST_SENTENCE.length - 1 - fixed.length;
145
192
  }
146
193
 
147
194
  const LIST_SKILLS_DESCRIPTION =
@@ -158,13 +205,9 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
158
205
  name: 'list_skills',
159
206
  description:
160
207
  LIST_SKILLS_DESCRIPTION +
161
- (await availableSkillsLine(skillService, userEmail, skillsLineBudget(LIST_SKILLS_DESCRIPTION))),
208
+ (await availableSkillsLine(skillService, userEmail, skillsLineBudget('list_skills', LIST_SKILLS_INPUTS, LIST_SKILLS_DESCRIPTION))),
162
209
  path: '/api/agent/tools/list_skills',
163
- inputs: {
164
- type: 'object',
165
- properties: { branch: BRANCH_SKILLS_INPUT },
166
- additionalProperties: false,
167
- },
210
+ inputs: LIST_SKILLS_INPUTS,
168
211
  outputs: {
169
212
  type: 'object',
170
213
  properties: {
@@ -210,32 +253,9 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
210
253
  name: 'get_skill',
211
254
  description:
212
255
  GET_SKILL_DESCRIPTION +
213
- (await availableSkillsLine(skillService, userEmail, skillsLineBudget(GET_SKILL_DESCRIPTION))),
256
+ (await availableSkillsLine(skillService, userEmail, skillsLineBudget('get_skill', GET_SKILL_INPUTS, GET_SKILL_DESCRIPTION))),
214
257
  path: '/api/agent/tools/get_skill',
215
- inputs: {
216
- type: 'object',
217
- properties: {
218
- name: { type: 'string', minLength: 1, description: 'Skill name (its folder name, e.g. `rfi`).' },
219
- file: {
220
- type: 'string',
221
- description:
222
- 'Optional bundled file path relative to the skill folder (e.g. `scripts/build_xlsx.py`) — ' +
223
- 'fetch its content instead of the body.',
224
- },
225
- version: {
226
- type: 'string',
227
- description:
228
- 'Optional: the declared version to load (e.g. `1.4.0` — the `version` that `list_skills` ' +
229
- 'reports: `metadata.version`, else `version`, else `lifecycle.version`). Omitted, the skill is loaded as it ' +
230
- 'is now, which is the latest. Given, the skill — or the `file` — is served as it was at the most ' +
231
- 'recent commit that declared that version; a version the skill never declared answers ' +
232
- '`version_not_found` with the versions it did declare.',
233
- },
234
- branch: BRANCH_SKILLS_INPUT,
235
- },
236
- required: ['name'],
237
- additionalProperties: false,
238
- },
258
+ inputs: GET_SKILL_INPUTS,
239
259
  outputs: {
240
260
  type: 'object',
241
261
  description: 'On success carries `skill` (or `file` when `file` was passed); on failure carries `error`.',
@@ -11,6 +11,7 @@ import type { AuthService } from '../../auth/auth.service.js';
11
11
  import { InternalTokenService } from '../internal-token.service.js';
12
12
  import { createManualAuthMiddleware, createToolAuthMiddleware } from '../tool-auth.middleware.js';
13
13
  import type { IExternalApiKeyService } from '../external-api-key.interface.js';
14
+ import { ACCOUNT_DEACTIVATED_MESSAGE } from '../../auth/account-admission.js';
14
15
 
15
16
  /**
16
17
  * A bearer shaped like a connection key that does not verify is answered with
@@ -24,6 +25,8 @@ import type { IExternalApiKeyService } from '../external-api-key.interface.js';
24
25
  * server requests, with the same gate `create-core-server.ts` puts on each.
25
26
  */
26
27
  const BAD_KEY = 'bevel_revokedKeyThatMustNeverBeEchoed';
28
+ /** A second refused key: on the switched-off service it is not one of a switched-off account. */
29
+ const OTHER_BAD_KEY = 'bevel_unknownKeyThatMustNeverBeEchoed';
27
30
  const RESOURCE_METADATA_URL = 'https://hexis.example/.well-known/oauth-protected-resource/api/mcp';
28
31
  const DISCOVERY_CHALLENGE = `Bearer realm="bevel-mcp", resource_metadata="${RESOURCE_METADATA_URL}"`;
29
32
  const KEY_CHALLENGE = 'Bearer error="invalid_token", error_description="Invalid or revoked connection key"';
@@ -190,3 +193,32 @@ describe('the REST agent surface keeps its challenge for everything but a key',
190
193
  }
191
194
  });
192
195
  });
196
+
197
+ describe('a live connection key whose account is switched off is told so', () => {
198
+ const switchedOffKeys = {
199
+ ...externalApiKeys,
200
+ isKeyOfSwitchedOffAccount: async (t: string) => t === BAD_KEY,
201
+ } as unknown as IExternalApiKeyService;
202
+
203
+ for (const endpoint of KEY_ENDPOINTS) {
204
+ it(`${endpoint.name}: 401 with the switched-off reason, not an invitation to mint a new key`, async () => {
205
+ const base = await start(switchedOffKeys);
206
+ const res = await call(base, endpoint.method, endpoint.path, BAD_KEY);
207
+ expect(res.status).toBe(401);
208
+ expect(res.headers.get('www-authenticate')).toBe(
209
+ 'Bearer error="invalid_token", error_description="The account of this connection key is switched off"',
210
+ );
211
+ expect(await res.json()).toEqual({ error: ACCOUNT_DEACTIVATED_MESSAGE });
212
+ expect(logged.join('\n')).not.toContain(BAD_KEY);
213
+ });
214
+
215
+ it(`${endpoint.name}: any other refused key on the same service is still told it is invalid`, async () => {
216
+ const base = await start(switchedOffKeys);
217
+ const res = await call(base, endpoint.method, endpoint.path, OTHER_BAD_KEY);
218
+ expect(res.status).toBe(401);
219
+ expect(res.headers.get('www-authenticate')).toBe(KEY_CHALLENGE);
220
+ expect(await res.json()).toEqual(KEY_BODY);
221
+ expect(logged.join('\n')).not.toContain(OTHER_BAD_KEY);
222
+ });
223
+ }
224
+ });
@@ -276,6 +276,71 @@ describe('ExternalApiKeyService', () => {
276
276
  expect(warn).toHaveBeenCalled();
277
277
  warn.mockRestore();
278
278
  });
279
+
280
+ /**
281
+ * A key's FIRST use is an agent arriving — what the onboarding waits for
282
+ * — so the owner is told once the stamp has landed, and once only. The
283
+ * first stamp is a CLAIM (`… where last_used_at is null … returning`):
284
+ * the request the database hands the row to announces, a racing one
285
+ * loses the claim, stamps the ordinary way and says nothing, and a key
286
+ * stamped before is touched without a word.
287
+ */
288
+ it("announces a key's first use to its owner once the claim has landed, and never otherwise", async () => {
289
+ const row = { tokenId: 'tok-1', label: 'Laptop', lastUsedAt: null, userId: 'user-7', email: 'a@x.io', name: 'Alice', avatarUrl: null };
290
+ const events = { emit: vi.fn() };
291
+ // The claim's answer is held back, to pin that the announcement waits for it.
292
+ let claimed!: (rows: { id: string }[]) => void;
293
+ const claim = new Promise<{ id: string }[]>((resolve) => (claimed = resolve));
294
+ const first = makeFakeDb([[row], claim]);
295
+ await new ExternalApiKeyService(first.db, 'bevel_', {}, events).verifyAndLoadUser('bevel_abc');
296
+ await flushMicrotasks();
297
+ expect(first.calls.update).toHaveLength(1);
298
+ expect(first.calls.set[0][0]).toHaveProperty('lastUsedAt');
299
+ expect(renderSql(first.calls.where[1][0]).sql).toMatch(/"api_tokens"."id" = \$1 and "api_tokens"."last_used_at" is null/);
300
+ expect(events.emit).not.toHaveBeenCalled();
301
+ claimed([{ id: 'tok-1' }]);
302
+ await flushMicrotasks();
303
+ expect(events.emit).toHaveBeenCalledTimes(1);
304
+ expect(events.emit).toHaveBeenCalledWith({
305
+ kind: 'agent-connected',
306
+ forUserId: 'user-7',
307
+ client: 'Laptop',
308
+ agentKind: 'key',
309
+ at: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
310
+ });
311
+
312
+ // A racing request lost the claim: the ordinary stamp, and no announcement.
313
+ const lost = makeFakeDb([[row], [], undefined]);
314
+ const lostEvents = { emit: vi.fn() };
315
+ await new ExternalApiKeyService(lost.db, 'bevel_', {}, lostEvents).verifyAndLoadUser('bevel_abc');
316
+ await flushMicrotasks();
317
+ expect(lost.calls.update).toHaveLength(2);
318
+ expect(lostEvents.emit).not.toHaveBeenCalled();
319
+
320
+ // A key stamped before: touched, and nobody is told.
321
+ const again = makeFakeDb([[{ ...row, lastUsedAt: new Date('2026-10-07T12:00:00Z') }], undefined]);
322
+ const laterEvents = { emit: vi.fn() };
323
+ await new ExternalApiKeyService(again.db, 'bevel_', {}, laterEvents).verifyAndLoadUser('bevel_abc');
324
+ await flushMicrotasks();
325
+ expect(again.calls.update).toHaveLength(1);
326
+ expect(laterEvents.emit).not.toHaveBeenCalled();
327
+ });
328
+
329
+ it('announces nothing when the first stamp could not be written', async () => {
330
+ const row = { tokenId: 'tok-1', label: 'Laptop', lastUsedAt: null, userId: 'user-7', email: 'a@x.io', name: 'Alice', avatarUrl: null };
331
+ let fail!: (err: Error) => void;
332
+ const claim = new Promise<{ id: string }[]>((_resolve, reject) => (fail = reject));
333
+ const { db } = makeFakeDb([[row], claim]);
334
+ const events = { emit: vi.fn() };
335
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
336
+ const user = await new ExternalApiKeyService(db, 'bevel_', {}, events).verifyAndLoadUser('bevel_abc');
337
+ expect(user?.id).toBe('user-7');
338
+ fail(new Error('connection lost'));
339
+ await flushMicrotasks();
340
+ expect(events.emit).not.toHaveBeenCalled();
341
+ expect(warn).toHaveBeenCalled();
342
+ warn.mockRestore();
343
+ });
279
344
  });
280
345
 
281
346
  describe('listForUser', () => {
@@ -1,4 +1,5 @@
1
1
  import type { Response } from 'express';
2
+ import { ACCOUNT_DEACTIVATED_MESSAGE } from '../auth/account-admission.js';
2
3
 
3
4
  /**
4
5
  * How every key-authenticated endpoint answers a bearer that LOOKS like a
@@ -19,7 +20,16 @@ export const INVALID_CONNECTION_KEY_CHALLENGE =
19
20
  export const INVALID_CONNECTION_KEY_MESSAGE =
20
21
  'Invalid or revoked connection key. Mint a new one in External agent access.';
21
22
 
22
- export function rejectConnectionKey(res: Response): void {
23
- res.setHeader('WWW-Authenticate', INVALID_CONNECTION_KEY_CHALLENGE);
24
- res.status(401).json({ error: INVALID_CONNECTION_KEY_MESSAGE });
23
+ /**
24
+ * A live key whose account an admin switched off: still a plain
25
+ * `invalid_token` (no sign-in invitation, see above), but told the real
26
+ * reason — telling them to mint a new key would send them to something
27
+ * they cannot do.
28
+ */
29
+ export const SWITCHED_OFF_CONNECTION_KEY_CHALLENGE =
30
+ 'Bearer error="invalid_token", error_description="The account of this connection key is switched off"';
31
+
32
+ export function rejectConnectionKey(res: Response, opts: { switchedOff?: boolean } = {}): void {
33
+ res.setHeader('WWW-Authenticate', opts.switchedOff ? SWITCHED_OFF_CONNECTION_KEY_CHALLENGE : INVALID_CONNECTION_KEY_CHALLENGE);
34
+ res.status(401).json({ error: opts.switchedOff ? ACCOUNT_DEACTIVATED_MESSAGE : INVALID_CONNECTION_KEY_MESSAGE });
25
35
  }
@@ -140,6 +140,15 @@ export interface IExternalApiKeyService {
140
140
  plaintext: string,
141
141
  ): Promise<{ tokenId: string; user: AuthUser } | null>;
142
142
 
143
+ /**
144
+ * For a key {@link verifyAndLoadToken} refused: whether it is a live key
145
+ * whose account an admin switched off — the one refusal its holder must
146
+ * not be told to mint a new key over (they cannot; the account is off).
147
+ * Asked only on the refusal path, so a working key costs nothing more.
148
+ * Optional: without it every refused key is reported as invalid.
149
+ */
150
+ isKeyOfSwitchedOffAccount?(plaintext: string): Promise<boolean>;
151
+
143
152
  /** Active + revoked tokens for the user, newest-first. */
144
153
  listForUser(userId: string): Promise<ExternalApiKeySummary[]>;
145
154
 
@@ -3,7 +3,7 @@ import { logger } from '../../shared/logging.js';
3
3
 
4
4
  const log = logger('external-api-key');
5
5
  import { and, desc, eq, isNotNull, isNull, type SQL } from 'drizzle-orm';
6
- import type { AuthUser } from '@bevel-software/platform-shared';
6
+ import type { AgentConnectedEvent, AuthUser } from '@bevel-software/platform-shared';
7
7
  import type { Database } from '../database/connection.js';
8
8
  import { externalApiKeys, users } from '../database/schema.js';
9
9
  import {
@@ -51,6 +51,12 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
51
51
  private readonly db: Database,
52
52
  private readonly keyPrefix: string,
53
53
  private readonly kinds: Readonly<Record<string, KeyKindSpec>> = {},
54
+ /**
55
+ * Where a key's FIRST use is announced to its owner (`agent-connected`,
56
+ * see `verifyAndLoadToken`). Optional: without it the stamp is written
57
+ * and nobody is told.
58
+ */
59
+ private readonly events?: { emit(event: AgentConnectedEvent): void },
54
60
  ) {
55
61
  this.prefixes = [keyPrefix, ...Object.values(kinds).map((k) => k.prefix)];
56
62
  }
@@ -97,6 +103,19 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
97
103
  return resolved ? resolved.user : null;
98
104
  }
99
105
 
106
+ async isKeyOfSwitchedOffAccount(plaintext: string): Promise<boolean> {
107
+ if (!this.looksLikeExternalApiKey(plaintext)) return false;
108
+ // The same key `verifyAndLoadToken` would accept (known, not revoked),
109
+ // but on an account that is off.
110
+ const [row] = await this.db
111
+ .select({ id: externalApiKeys.id })
112
+ .from(externalApiKeys)
113
+ .innerJoin(users, eq(externalApiKeys.userId, users.id))
114
+ .where(and(eq(externalApiKeys.tokenHash, hashToken(plaintext)), isNull(externalApiKeys.revokedAt), isNotNull(users.deactivatedAt)))
115
+ .limit(1);
116
+ return row !== undefined;
117
+ }
118
+
100
119
  async verifyAndLoadToken(
101
120
  plaintext: string,
102
121
  ): Promise<{ tokenId: string; user: AuthUser } | null> {
@@ -114,6 +133,8 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
114
133
  const [row] = await this.db
115
134
  .select({
116
135
  tokenId: externalApiKeys.id,
136
+ label: externalApiKeys.label,
137
+ lastUsedAt: externalApiKeys.lastUsedAt,
117
138
  userId: users.id,
118
139
  email: users.email,
119
140
  name: users.name,
@@ -130,9 +151,28 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
130
151
  // the UPDATE. A failed touch is logged but never blocks the caller —
131
152
  // the worst case is a slightly stale `last_used_at`, which is fine for
132
153
  // a "when was this key last used" audit view.
133
- this.touchLastUsed(row.tokenId).catch((err) => {
134
- log.warn('touchLastUsed failed:', { err });
135
- });
154
+ //
155
+ // A key's FIRST use is an agent that has just reached the platform —
156
+ // what the onboarding waits for — so its owner is told once the stamp
157
+ // has landed. A key never stamped is CLAIMED (`claimFirstUse`), so of
158
+ // two requests racing its first use exactly one announces it; a key
159
+ // stamped before is touched and nobody is told.
160
+ const stamped = row.lastUsedAt === null ? this.claimFirstUse(row.tokenId) : this.touchLastUsed(row.tokenId).then(() => false);
161
+ stamped.then(
162
+ (first) => {
163
+ if (!first) return;
164
+ this.events?.emit({
165
+ kind: 'agent-connected',
166
+ forUserId: row.userId,
167
+ client: row.label,
168
+ agentKind: 'key',
169
+ at: new Date().toISOString(),
170
+ });
171
+ },
172
+ (err) => {
173
+ log.warn('touchLastUsed failed:', { err });
174
+ },
175
+ );
136
176
 
137
177
  return {
138
178
  tokenId: row.tokenId,
@@ -280,6 +320,23 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
280
320
  .set({ lastUsedAt: new Date() })
281
321
  .where(eq(externalApiKeys.id, tokenId));
282
322
  }
323
+
324
+ /**
325
+ * The first stamp as ONE statement: the row is taken only while
326
+ * `last_used_at` is still null, so of two requests (or two replicas)
327
+ * racing a key's first use the database hands it to exactly one, which
328
+ * answers true. The loser stamps the ordinary way and answers false.
329
+ */
330
+ private async claimFirstUse(tokenId: string): Promise<boolean> {
331
+ const won = await this.db
332
+ .update(externalApiKeys)
333
+ .set({ lastUsedAt: new Date() })
334
+ .where(and(eq(externalApiKeys.id, tokenId), isNull(externalApiKeys.lastUsedAt)))
335
+ .returning({ id: externalApiKeys.id });
336
+ if (won.length > 0) return true;
337
+ await this.touchLastUsed(tokenId);
338
+ return false;
339
+ }
283
340
  }
284
341
 
285
342
  function hashToken(plaintext: string): string {
@@ -6,7 +6,11 @@ import type { IExternalApiKeyService } from './external-api-key.interface.js';
6
6
  import type { AuthService } from '../auth/auth.service.js';
7
7
  import { ACCOUNT_DEACTIVATED_MESSAGE, AuthBackendError } from '../auth/account-admission.js';
8
8
  import { InternalTokenService } from './internal-token.service.js';
9
- import { INVALID_CONNECTION_KEY_CHALLENGE, INVALID_CONNECTION_KEY_MESSAGE } from './connection-key-rejection.js';
9
+ import {
10
+ INVALID_CONNECTION_KEY_CHALLENGE,
11
+ INVALID_CONNECTION_KEY_MESSAGE,
12
+ SWITCHED_OFF_CONNECTION_KEY_CHALLENGE,
13
+ } from './connection-key-rejection.js';
10
14
 
11
15
  /**
12
16
  * What the `toolAuth` middleware resolves a bearer to, before the per-call
@@ -165,9 +169,14 @@ export function createTokenVerifier(
165
169
  // External API key → external caller. (verifyAndLoadToken may throw → propagates → caller maps to 500.)
166
170
  if (externalApiKeyService.looksLikeExternalApiKey(token)) {
167
171
  const resolved = await externalApiKeyService.verifyAndLoadToken(token);
168
- return resolved
169
- ? { ok: true, auth: { source: 'external', userId: resolved.user.id, tokenId: resolved.tokenId, scope: 'write' } }
170
- : { ok: false, status: 401, message: INVALID_CONNECTION_KEY_MESSAGE, challenge: INVALID_CONNECTION_KEY_CHALLENGE };
172
+ if (resolved) {
173
+ return { ok: true, auth: { source: 'external', userId: resolved.user.id, tokenId: resolved.tokenId, scope: 'write' } };
174
+ }
175
+ // A live key of a switched-off account is told so, not sent to mint another.
176
+ if (await externalApiKeyService.isKeyOfSwitchedOffAccount?.(token)) {
177
+ return { ok: false, status: 401, message: ACCOUNT_DEACTIVATED_MESSAGE, challenge: SWITCHED_OFF_CONNECTION_KEY_CHALLENGE };
178
+ }
179
+ return { ok: false, status: 401, message: INVALID_CONNECTION_KEY_MESSAGE, challenge: INVALID_CONNECTION_KEY_CHALLENGE };
171
180
  }
172
181
 
173
182
  return missing;