@bevel-software/platform-core-backend 0.34.0 → 0.36.1

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 (908) hide show
  1. package/agent-guide/access-control.md +146 -300
  2. package/agent-guide/conventions.md +23 -25
  3. package/agent-guide/directory-structure.md +72 -120
  4. package/agent-guide/drafts-and-change-requests.md +92 -0
  5. package/agent-guide/for-git-users.md +47 -0
  6. package/agent-guide/glossary.md +36 -0
  7. package/agent-guide/how-agents-find-things.md +49 -0
  8. package/agent-guide/html-views.md +16 -11
  9. package/agent-guide/new-knowledge-base.md +9 -8
  10. package/agent-guide/onboarding.md +111 -0
  11. package/agent-guide/skills.md +67 -42
  12. package/agent-guide/start-here.md +66 -0
  13. package/agent-guide/tool-manuals.md +133 -154
  14. package/agent-guide/tools.md +67 -0
  15. package/agent-guide/where-a-new-file-goes.md +45 -28
  16. package/dist/core/catalog-cache-invalidation.d.ts +2 -2
  17. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
  18. package/dist/core/compose-embed.d.ts +2 -2
  19. package/dist/core/compose-embed.d.ts.map +1 -1
  20. package/dist/core/compose-embed.js +1 -1
  21. package/dist/core/compose-embed.js.map +1 -1
  22. package/dist/core/core-ports.d.ts +19 -4
  23. package/dist/core/core-ports.d.ts.map +1 -1
  24. package/dist/core/core-ports.js.map +1 -1
  25. package/dist/core/create-core-server.d.ts.map +1 -1
  26. package/dist/core/create-core-server.js +27 -25
  27. package/dist/core/create-core-server.js.map +1 -1
  28. package/dist/core/create-core-services.d.ts +25 -13
  29. package/dist/core/create-core-services.d.ts.map +1 -1
  30. package/dist/core/create-core-services.js +75 -60
  31. package/dist/core/create-core-services.js.map +1 -1
  32. package/dist/core/lifecycle.d.ts +4 -4
  33. package/dist/core/lifecycle.d.ts.map +1 -1
  34. package/dist/core/lifecycle.js +5 -5
  35. package/dist/core/lifecycle.js.map +1 -1
  36. package/dist/core-config.d.ts +2 -2
  37. package/dist/core-config.js +3 -3
  38. package/dist/core-config.js.map +1 -1
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/modules/access/access-control.interface.d.ts +22 -0
  44. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  45. package/dist/modules/access/access-control.service.d.ts +3 -1
  46. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  47. package/dist/modules/access/access-control.service.js +76 -1
  48. package/dist/modules/access/access-control.service.js.map +1 -1
  49. package/dist/modules/access/access-declarations.d.ts +2 -2
  50. package/dist/modules/access/access-declarations.d.ts.map +1 -1
  51. package/dist/modules/access/access-declarations.js +2 -2
  52. package/dist/modules/access/access-declarations.js.map +1 -1
  53. package/dist/modules/access/access-mutation.service.d.ts +2 -2
  54. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  55. package/dist/modules/access/access-mutation.service.js +2 -2
  56. package/dist/modules/access/access-mutation.service.js.map +1 -1
  57. package/dist/modules/access/access-requests.routes.d.ts +2 -2
  58. package/dist/modules/access/access-requests.routes.d.ts.map +1 -1
  59. package/dist/modules/access/access-requests.routes.js +5 -5
  60. package/dist/modules/access/access-requests.routes.js.map +1 -1
  61. package/dist/modules/access/access-requests.service.d.ts +2 -2
  62. package/dist/modules/access/access-requests.service.d.ts.map +1 -1
  63. package/dist/modules/access/access-requests.service.js +11 -11
  64. package/dist/modules/access/access-requests.service.js.map +1 -1
  65. package/dist/modules/access/access.routes.d.ts +2 -2
  66. package/dist/modules/access/access.routes.d.ts.map +1 -1
  67. package/dist/modules/access/access.routes.js +32 -13
  68. package/dist/modules/access/access.routes.js.map +1 -1
  69. package/dist/modules/access/admin-locked-commit.d.ts +5 -5
  70. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  71. package/dist/modules/access/admin-locked-commit.js +7 -7
  72. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  73. package/dist/modules/access/admin-route-helpers.d.ts +1 -1
  74. package/dist/modules/access/admin-route-helpers.js +5 -5
  75. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  76. package/dist/modules/access/change-read-gate.d.ts +1 -1
  77. package/dist/modules/access/change-read-gate.js +1 -1
  78. package/dist/modules/access/groups-admin.service.d.ts +7 -7
  79. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  80. package/dist/modules/access/groups-admin.service.js +6 -6
  81. package/dist/modules/access/groups-admin.service.js.map +1 -1
  82. package/dist/modules/access/index.d.ts +1 -1
  83. package/dist/modules/access/index.d.ts.map +1 -1
  84. package/dist/modules/access/reference-scan.d.ts +1 -1
  85. package/dist/modules/access/roles-admin.service.d.ts +7 -7
  86. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  87. package/dist/modules/access/roles-admin.service.js +8 -8
  88. package/dist/modules/access/roles-admin.service.js.map +1 -1
  89. package/dist/modules/access/synced-groups-committer.d.ts +4 -4
  90. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  91. package/dist/modules/access/synced-groups-committer.js +7 -7
  92. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  93. package/dist/modules/access/user-access-removal.service.d.ts +6 -6
  94. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -1
  95. package/dist/modules/access/user-access-removal.service.js +4 -4
  96. package/dist/modules/access/user-access-removal.service.js.map +1 -1
  97. package/dist/modules/access-model/access-errors.d.ts +4 -4
  98. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-errors.js +4 -4
  100. package/dist/modules/access-model/access-errors.js.map +1 -1
  101. package/dist/modules/access-model/access-grammar.d.ts +1 -1
  102. package/dist/modules/access-model/access-grammar.js +4 -4
  103. package/dist/modules/access-model/change-gate.d.ts +1 -1
  104. package/dist/modules/access-model/change-gate.js +1 -1
  105. package/dist/modules/access-model/roles-yaml-guard.d.ts +4 -4
  106. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  107. package/dist/modules/access-model/roles-yaml-guard.js +4 -4
  108. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  109. package/dist/modules/agent-guide/agent-guide.d.ts +63 -18
  110. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  111. package/dist/modules/agent-guide/agent-guide.js +144 -30
  112. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  113. package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
  114. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  115. package/dist/modules/agent-guide/agent-guide.tools.js +45 -31
  116. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  117. package/dist/modules/agent-guide/guide-budget.d.ts +51 -0
  118. package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
  119. package/dist/modules/agent-guide/guide-budget.js +71 -0
  120. package/dist/modules/agent-guide/guide-budget.js.map +1 -0
  121. package/dist/modules/agent-guide/index.d.ts +2 -1
  122. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  123. package/dist/modules/agent-guide/index.js +2 -1
  124. package/dist/modules/agent-guide/index.js.map +1 -1
  125. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +12 -5
  126. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  127. package/dist/modules/agent-instructions/agent-instructions.routes.js +13 -6
  128. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  129. package/dist/modules/agent-instructions/compose.d.ts +15 -8
  130. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  131. package/dist/modules/agent-instructions/compose.js +28 -10
  132. package/dist/modules/agent-instructions/compose.js.map +1 -1
  133. package/dist/modules/agent-instructions/shared-file-rules.d.ts +14 -6
  134. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  135. package/dist/modules/agent-instructions/shared-file-rules.js +60 -18
  136. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  137. package/dist/modules/auth/account-erasure.service.d.ts +36 -7
  138. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  139. package/dist/modules/auth/account-erasure.service.js +30 -10
  140. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  141. package/dist/modules/auth/account-hooks.d.ts +20 -0
  142. package/dist/modules/auth/account-hooks.d.ts.map +1 -1
  143. package/dist/modules/auth/account-hooks.js +10 -0
  144. package/dist/modules/auth/account-hooks.js.map +1 -1
  145. package/dist/modules/auth/account-switch-lock.js +2 -2
  146. package/dist/modules/auth/account-switch-lock.js.map +1 -1
  147. package/dist/modules/auth/account.routes.d.ts +7 -2
  148. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  149. package/dist/modules/auth/account.routes.js +23 -4
  150. package/dist/modules/auth/account.routes.js.map +1 -1
  151. package/dist/modules/auth/auth.service.d.ts +1 -1
  152. package/dist/modules/auth/auth.service.js +4 -4
  153. package/dist/modules/auth/auth.service.js.map +1 -1
  154. package/dist/modules/auth/index.d.ts +1 -0
  155. package/dist/modules/auth/index.d.ts.map +1 -1
  156. package/dist/modules/code-mode/code-mode.tool.js +2 -2
  157. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  158. package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
  159. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  160. package/dist/modules/connection-probe/connection-probe.service.js +1 -38
  161. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  162. package/dist/modules/database/core-schema.js +2 -2
  163. package/dist/modules/database/core-schema.js.map +1 -1
  164. package/dist/modules/diff/diff.routes.d.ts +3 -3
  165. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  166. package/dist/modules/diff/diff.routes.js +7 -7
  167. package/dist/modules/diff/diff.routes.js.map +1 -1
  168. package/dist/modules/embed/embed.routes.js +1 -1
  169. package/dist/modules/embed/embed.routes.js.map +1 -1
  170. package/dist/modules/embed/embed.service.d.ts +4 -4
  171. package/dist/modules/embed/embed.service.d.ts.map +1 -1
  172. package/dist/modules/embed/embed.service.js +18 -18
  173. package/dist/modules/embed/embed.service.js.map +1 -1
  174. package/dist/modules/kb-fs/branch-name.js +8 -8
  175. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  176. package/dist/modules/kb-fs/file-change-notifier.d.ts +2 -2
  177. package/dist/modules/kb-fs/locking-filesystem.d.ts +9 -9
  178. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  179. package/dist/modules/kb-fs/locking-filesystem.js +27 -27
  180. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  181. package/dist/modules/kb-fs/repo-path.js +5 -5
  182. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  183. package/dist/modules/kb-fs/tmp-ref.d.ts +2 -2
  184. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -1
  185. package/dist/modules/kb-fs/tmp-ref.js +2 -2
  186. package/dist/modules/kb-fs/tmp-ref.js.map +1 -1
  187. package/dist/modules/kb-sync/index.d.ts +1 -1
  188. package/dist/modules/kb-sync/index.d.ts.map +1 -1
  189. package/dist/modules/kb-sync/kb-sync.interface.d.ts +3 -3
  190. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  191. package/dist/modules/kb-sync/kb-sync.service.d.ts +3 -3
  192. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  193. package/dist/modules/kb-sync/kb-sync.service.js +10 -10
  194. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  195. package/dist/modules/mcp/mcp.service.d.ts +11 -4
  196. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  197. package/dist/modules/mcp/mcp.service.js +80 -53
  198. package/dist/modules/mcp/mcp.service.js.map +1 -1
  199. package/dist/modules/onboarding/onboarding.contract.d.ts +1 -1
  200. package/dist/modules/onboarding/onboarding.routes.js +2 -2
  201. package/dist/modules/onboarding/onboarding.routes.js.map +1 -1
  202. package/dist/modules/onboarding/starter-pack.service.d.ts +4 -4
  203. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -1
  204. package/dist/modules/onboarding/starter-pack.service.js +4 -4
  205. package/dist/modules/onboarding/starter-pack.service.js.map +1 -1
  206. package/dist/modules/plugins/join-request-jobs.service.d.ts +2 -2
  207. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
  208. package/dist/modules/plugins/join-request-jobs.service.js +7 -7
  209. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
  210. package/dist/modules/plugins/join-requests.service.d.ts +3 -3
  211. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  212. package/dist/modules/plugins/join-requests.service.js +5 -5
  213. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  214. package/dist/modules/plugins/plugin-rename.service.js +1 -1
  215. package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
  216. package/dist/modules/plugins/plugins.routes.d.ts +2 -2
  217. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  218. package/dist/modules/plugins/plugins.routes.js +8 -8
  219. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  220. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +87 -2
  221. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  222. package/dist/modules/secrets-vault/db-secrets-vault.service.js +635 -74
  223. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  224. package/dist/modules/secrets-vault/index.d.ts +3 -2
  225. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  226. package/dist/modules/secrets-vault/index.js +2 -1
  227. package/dist/modules/secrets-vault/index.js.map +1 -1
  228. package/dist/modules/secrets-vault/managed-sign-in-erasure.d.ts +36 -0
  229. package/dist/modules/secrets-vault/managed-sign-in-erasure.d.ts.map +1 -0
  230. package/dist/modules/secrets-vault/managed-sign-in-erasure.js +37 -0
  231. package/dist/modules/secrets-vault/managed-sign-in-erasure.js.map +1 -0
  232. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +205 -11
  233. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  234. package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
  235. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  236. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +7 -1
  237. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  238. package/dist/modules/secrets-vault/secrets-vault.routes.js +206 -45
  239. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  240. package/dist/modules/settings/connection-check.js +1 -1
  241. package/dist/modules/settings/connection-check.js.map +1 -1
  242. package/dist/modules/settings/deployment-settings.service.js +1 -1
  243. package/dist/modules/settings/git-root-folders.js +1 -1
  244. package/dist/modules/settings/git-root-folders.js.map +1 -1
  245. package/dist/modules/settings/oidc-check.d.ts.map +1 -1
  246. package/dist/modules/settings/oidc-check.js +5 -17
  247. package/dist/modules/settings/oidc-check.js.map +1 -1
  248. package/dist/modules/settings/setup.routes.d.ts +1 -1
  249. package/dist/modules/settings/setup.routes.js +1 -1
  250. package/dist/modules/skills/pending-skills.service.d.ts +3 -3
  251. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  252. package/dist/modules/skills/pending-skills.service.js +4 -4
  253. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  254. package/dist/modules/skills/skill-access-requests.routes.d.ts +2 -2
  255. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  256. package/dist/modules/skills/skill-access-requests.routes.js +5 -5
  257. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  258. package/dist/modules/skills/skills.tools.js +1 -1
  259. package/dist/modules/skills/skills.tools.js.map +1 -1
  260. package/dist/modules/tool-helpers/tool-context.d.ts +4 -4
  261. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  262. package/dist/modules/tool-helpers/tool-context.js +2 -2
  263. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  264. package/dist/modules/tool-helpers/tool-def.js +1 -1
  265. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  266. package/dist/modules/tool-helpers/tool-handler.js +2 -2
  267. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  268. package/dist/modules/tool-helpers/tool.contract.d.ts +4 -4
  269. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  270. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
  271. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  272. package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
  273. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  274. package/dist/modules/tool-manuals/mcp-server-edit.service.js +1 -1
  275. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  276. package/dist/modules/tool-manuals/pending-tools.service.d.ts +3 -3
  277. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -1
  278. package/dist/modules/tool-manuals/pending-tools.service.js +4 -4
  279. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -1
  280. package/dist/modules/tool-manuals/tool-delete.service.js +1 -1
  281. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -1
  282. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +70 -3
  283. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  284. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +18 -8
  285. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  286. package/dist/modules/tool-manuals/tool-manuals.service.js +166 -49
  287. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  288. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +11 -2
  289. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  290. package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -24
  291. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  292. package/dist/modules/version-control/agent-tools/change-request-read-shape.d.ts.map +1 -0
  293. package/dist/modules/version-control/agent-tools/change-request-read-shape.js.map +1 -0
  294. package/dist/modules/version-control/agent-tools/change-request-read.tools.d.ts.map +1 -0
  295. package/dist/modules/{workflow → version-control}/agent-tools/change-request-read.tools.js +2 -2
  296. package/dist/modules/version-control/agent-tools/change-request-read.tools.js.map +1 -0
  297. package/dist/modules/version-control/agent-tools/change-request-summary.d.ts.map +1 -0
  298. package/dist/modules/version-control/agent-tools/change-request-summary.js.map +1 -0
  299. package/dist/modules/{workflow/agent-tools/workflow.tools.d.ts → version-control/agent-tools/version-control.tools.d.ts} +4 -4
  300. package/dist/modules/version-control/agent-tools/version-control.tools.d.ts.map +1 -0
  301. package/dist/modules/{workflow/agent-tools/workflow.tools.js → version-control/agent-tools/version-control.tools.js} +53 -49
  302. package/dist/modules/version-control/agent-tools/version-control.tools.js.map +1 -0
  303. package/dist/modules/{workflow → version-control}/change-request-hooks.d.ts +1 -1
  304. package/dist/modules/version-control/change-request-hooks.d.ts.map +1 -0
  305. package/dist/modules/version-control/change-request-hooks.js.map +1 -0
  306. package/dist/modules/version-control/change-request-review/approval-lock.d.ts.map +1 -0
  307. package/dist/modules/version-control/change-request-review/approval-lock.js.map +1 -0
  308. package/dist/modules/{workflow/review-workflow/review-workflow.interface.d.ts → version-control/change-request-review/change-request-review.interface.d.ts} +2 -2
  309. package/dist/modules/version-control/change-request-review/change-request-review.interface.d.ts.map +1 -0
  310. package/dist/modules/version-control/change-request-review/change-request-review.interface.js +2 -0
  311. package/dist/modules/version-control/change-request-review/change-request-review.interface.js.map +1 -0
  312. package/dist/modules/{workflow/review-workflow/review-workflow.service.d.ts → version-control/change-request-review/change-request-review.service.d.ts} +3 -3
  313. package/dist/modules/version-control/change-request-review/change-request-review.service.d.ts.map +1 -0
  314. package/dist/modules/{workflow/review-workflow/review-workflow.service.js → version-control/change-request-review/change-request-review.service.js} +26 -26
  315. package/dist/modules/version-control/change-request-review/change-request-review.service.js.map +1 -0
  316. package/dist/modules/{workflow → version-control}/event-bus.d.ts +8 -8
  317. package/dist/modules/version-control/event-bus.d.ts.map +1 -0
  318. package/dist/modules/{workflow → version-control}/event-bus.js +4 -4
  319. package/dist/modules/version-control/event-bus.js.map +1 -0
  320. package/dist/modules/{workflow → version-control}/events-delivery.d.ts +4 -4
  321. package/dist/modules/version-control/events-delivery.d.ts.map +1 -0
  322. package/dist/modules/version-control/events-delivery.js.map +1 -0
  323. package/dist/modules/{workflow → version-control}/events.routes.d.ts +4 -4
  324. package/dist/modules/version-control/events.routes.d.ts.map +1 -0
  325. package/dist/modules/version-control/events.routes.js.map +1 -0
  326. package/dist/modules/{workflow → version-control}/file-lock.service.d.ts +2 -2
  327. package/dist/modules/version-control/file-lock.service.d.ts.map +1 -0
  328. package/dist/modules/{workflow → version-control}/file-lock.service.js +6 -6
  329. package/dist/modules/version-control/file-lock.service.js.map +1 -0
  330. package/dist/modules/version-control/git/change-request-link.d.ts.map +1 -0
  331. package/dist/modules/version-control/git/change-request-link.js.map +1 -0
  332. package/dist/modules/version-control/git/git-version.d.ts.map +1 -0
  333. package/dist/modules/version-control/git/git-version.js.map +1 -0
  334. package/dist/modules/{workflow → version-control}/git/git.service.d.ts +8 -8
  335. package/dist/modules/version-control/git/git.service.d.ts.map +1 -0
  336. package/dist/modules/{workflow → version-control}/git/git.service.js +53 -53
  337. package/dist/modules/version-control/git/git.service.js.map +1 -0
  338. package/dist/modules/version-control/git/merge-commit.d.ts.map +1 -0
  339. package/dist/modules/version-control/git/merge-commit.js.map +1 -0
  340. package/dist/modules/version-control/git/node-git-runner.d.ts.map +1 -0
  341. package/dist/modules/{workflow → version-control}/git/node-git-runner.js +1 -1
  342. package/dist/modules/version-control/git/node-git-runner.js.map +1 -0
  343. package/dist/modules/{workflow → version-control}/git/pull-request.service.d.ts +5 -5
  344. package/dist/modules/version-control/git/pull-request.service.d.ts.map +1 -0
  345. package/dist/modules/{workflow → version-control}/git/pull-request.service.js +10 -10
  346. package/dist/modules/version-control/git/pull-request.service.js.map +1 -0
  347. package/dist/modules/{workflow → version-control}/index.d.ts +6 -5
  348. package/dist/modules/version-control/index.d.ts.map +1 -0
  349. package/dist/modules/{workflow → version-control}/index.js +3 -2
  350. package/dist/modules/version-control/index.js.map +1 -0
  351. package/dist/modules/{workflow → version-control}/pending-commits.service.d.ts +2 -2
  352. package/dist/modules/version-control/pending-commits.service.d.ts.map +1 -0
  353. package/dist/modules/{workflow → version-control}/pending-commits.service.js +2 -2
  354. package/dist/modules/version-control/pending-commits.service.js.map +1 -0
  355. package/dist/modules/{workflow → version-control}/pending-commits.worker.d.ts +5 -5
  356. package/dist/modules/version-control/pending-commits.worker.d.ts.map +1 -0
  357. package/dist/modules/{workflow → version-control}/pending-commits.worker.js +3 -3
  358. package/dist/modules/version-control/pending-commits.worker.js.map +1 -0
  359. package/dist/modules/{workflow → version-control}/read-on-branch.d.ts +2 -2
  360. package/dist/modules/version-control/read-on-branch.d.ts.map +1 -0
  361. package/dist/modules/{workflow → version-control}/read-on-branch.js +2 -2
  362. package/dist/modules/version-control/read-on-branch.js.map +1 -0
  363. package/dist/modules/version-control/recovery-bot.d.ts.map +1 -0
  364. package/dist/modules/version-control/recovery-bot.js.map +1 -0
  365. package/dist/modules/version-control/sanitize-error.d.ts.map +1 -0
  366. package/dist/modules/version-control/sanitize-error.js.map +1 -0
  367. package/dist/modules/{workflow/workflow-hooks.d.ts → version-control/version-control-hooks.d.ts} +4 -4
  368. package/dist/modules/version-control/version-control-hooks.d.ts.map +1 -0
  369. package/dist/modules/{workflow/workflow-hooks.js → version-control/version-control-hooks.js} +3 -3
  370. package/dist/modules/version-control/version-control-hooks.js.map +1 -0
  371. package/dist/modules/{workflow/workflow.routes.d.ts → version-control/version-control.routes.d.ts} +6 -5
  372. package/dist/modules/version-control/version-control.routes.d.ts.map +1 -0
  373. package/dist/modules/{workflow/workflow.routes.js → version-control/version-control.routes.js} +61 -60
  374. package/dist/modules/version-control/version-control.routes.js.map +1 -0
  375. package/dist/modules/{workflow/workflow.service.d.ts → version-control/version-control.service.d.ts} +26 -31
  376. package/dist/modules/version-control/version-control.service.d.ts.map +1 -0
  377. package/dist/modules/{workflow/workflow.service.js → version-control/version-control.service.js} +89 -94
  378. package/dist/modules/version-control/version-control.service.js.map +1 -0
  379. package/dist/modules/workspace/agent-access.gate.d.ts +6 -6
  380. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
  381. package/dist/modules/workspace/agent-access.gate.js +2 -2
  382. package/dist/modules/workspace/agent-access.gate.js.map +1 -1
  383. package/dist/modules/workspace/git-internals.middleware.d.ts +1 -1
  384. package/dist/modules/workspace/git-internals.middleware.js +2 -2
  385. package/dist/modules/workspace/kb-repository.js +1 -1
  386. package/dist/modules/workspace/kb-repository.js.map +1 -1
  387. package/dist/modules/workspace/startup/kb-git.d.ts +1 -1
  388. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  389. package/dist/modules/workspace/startup/kb-git.js +1 -1
  390. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  391. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +2 -2
  392. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  393. package/dist/modules/workspace/startup/steps/seed-tree.js +1 -1
  394. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  395. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +2 -2
  396. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  397. package/dist/modules/workspace/startup/steps/template-files.step.js +2 -2
  398. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  399. package/dist/modules/workspace/startup/steps/template-source.d.ts +17 -4
  400. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  401. package/dist/modules/workspace/startup/steps/template-source.js +8 -5
  402. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  403. package/dist/modules/workspace/workspace-tmp.js +3 -3
  404. package/dist/modules/workspace/workspace-tmp.js.map +1 -1
  405. package/dist/modules/workspace/workspace.routes.d.ts +4 -4
  406. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  407. package/dist/modules/workspace/workspace.routes.js +22 -22
  408. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  409. package/dist/modules/workspace/workspace.service.d.ts +6 -6
  410. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  411. package/dist/modules/workspace/workspace.service.js +11 -11
  412. package/dist/modules/workspace/workspace.service.js.map +1 -1
  413. package/dist/modules/workspace/workspace.tools.d.ts +5 -3
  414. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  415. package/dist/modules/workspace/workspace.tools.js +85 -30
  416. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  417. package/dist/shared/canonical-file-identity.d.ts +2 -2
  418. package/dist/shared/canonical-file-identity.js +5 -5
  419. package/dist/shared/canonical-file-identity.js.map +1 -1
  420. package/dist/shared/domain-errors.d.ts +39 -39
  421. package/dist/shared/domain-errors.d.ts.map +1 -1
  422. package/dist/shared/domain-errors.js +44 -44
  423. package/dist/shared/domain-errors.js.map +1 -1
  424. package/dist/shared/git.contract.d.ts +1 -1
  425. package/dist/shared/git.contract.js +1 -1
  426. package/dist/shared/http-errors.d.ts +3 -3
  427. package/dist/shared/http-errors.d.ts.map +1 -1
  428. package/dist/shared/http-errors.js +1 -1
  429. package/dist/shared/http-errors.js.map +1 -1
  430. package/dist/shared/oauth-token-error.d.ts +23 -0
  431. package/dist/shared/oauth-token-error.d.ts.map +1 -0
  432. package/dist/shared/oauth-token-error.js +47 -0
  433. package/dist/shared/oauth-token-error.js.map +1 -0
  434. package/dist/shared/pending-proposals.d.ts +3 -3
  435. package/dist/shared/pending-proposals.d.ts.map +1 -1
  436. package/dist/shared/pending-proposals.js +2 -2
  437. package/dist/shared/pending-proposals.js.map +1 -1
  438. package/dist/shared/read-capped.d.ts +19 -0
  439. package/dist/shared/read-capped.d.ts.map +1 -0
  440. package/dist/shared/read-capped.js +45 -0
  441. package/dist/shared/read-capped.js.map +1 -0
  442. package/dist/tenancy/static-tenant-source.js +1 -1
  443. package/dist/tenancy/static-tenant-source.js.map +1 -1
  444. package/migrations/0012_agent_audit.sql +40 -40
  445. package/migrations/0013_agent_identity_and_key_soft_delete.sql +29 -29
  446. package/migrations/meta/0012_snapshot.json +2246 -2246
  447. package/migrations/meta/0013_snapshot.json +2258 -2258
  448. package/package.json +5 -4
  449. package/src/__tests__/account-admin.ts +27 -0
  450. package/src/__tests__/hexis-catalog.ts +144 -0
  451. package/src/__tests__/kb-context.ts +30 -30
  452. package/src/__tests__/kb-layout-config.test.ts +21 -0
  453. package/src/__tests__/open-change-gate.ts +1 -1
  454. package/src/__tests__/real-disk.ts +21 -0
  455. package/src/__tests__/tokens.ts +19 -0
  456. package/src/__tests__/tool-handler.ts +15 -0
  457. package/src/__tests__/tool-manuals.ts +32 -0
  458. package/src/__tests__/tool-wiring.ts +31 -0
  459. package/src/__tests__/workspace-service.ts +1 -1
  460. package/src/core/__tests__/catalog-cache-invalidation.test.ts +3 -3
  461. package/src/core/__tests__/compose-embed.test.ts +1 -1
  462. package/src/core/__tests__/damaged-document.tools.test.ts +3 -3
  463. package/src/core/__tests__/erasure-slices-wired-once.test.ts +46 -0
  464. package/src/core/__tests__/gated-boot.test.ts +2 -2
  465. package/src/core/__tests__/lifecycle.test.ts +9 -9
  466. package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
  467. package/src/core/catalog-cache-invalidation.ts +2 -2
  468. package/src/core/compose-embed.ts +3 -3
  469. package/src/core/core-ports.ts +19 -4
  470. package/src/core/create-core-server.ts +40 -26
  471. package/src/core/create-core-services.ts +92 -66
  472. package/src/core/lifecycle.ts +9 -9
  473. package/src/core-config.ts +3 -3
  474. package/src/index.ts +5 -5
  475. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +4 -4
  476. package/src/modules/access/__tests__/access-own-read-grant.test.ts +14 -14
  477. package/src/modules/access/__tests__/access-requests.routes.test.ts +20 -20
  478. package/src/modules/access/__tests__/access.routes.admins.test.ts +4 -4
  479. package/src/modules/access/__tests__/access.routes.batch.test.ts +4 -4
  480. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +6 -6
  481. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -6
  482. package/src/modules/access/__tests__/access.routes.overrides.test.ts +5 -5
  483. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +6 -6
  484. package/src/modules/access/__tests__/access.routes.prospective.test.ts +54 -14
  485. package/src/modules/access/__tests__/access.routes.revoke.test.ts +6 -6
  486. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +6 -6
  487. package/src/modules/access/__tests__/admin-locked-commit.test.ts +6 -6
  488. package/src/modules/access/__tests__/admin-route-helpers.test.ts +5 -5
  489. package/src/modules/access/__tests__/groups-admin.service.test.ts +27 -27
  490. package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
  491. package/src/modules/access/__tests__/roles-admin.security.test.ts +14 -13
  492. package/src/modules/access/__tests__/roles-admin.service.test.ts +27 -27
  493. package/src/modules/access/__tests__/roles-capabilities.test.ts +19 -19
  494. package/src/modules/access/__tests__/roles.routes.test.ts +6 -6
  495. package/src/modules/access/__tests__/synced-groups-committer.test.ts +13 -13
  496. package/src/modules/access/__tests__/user-access-removal.service.test.ts +29 -29
  497. package/src/modules/access/access-control.interface.ts +26 -0
  498. package/src/modules/access/access-control.service.ts +93 -1
  499. package/src/modules/access/access-declarations.ts +2 -2
  500. package/src/modules/access/access-mutation.service.ts +2 -2
  501. package/src/modules/access/access-requests.routes.ts +7 -7
  502. package/src/modules/access/access-requests.service.ts +13 -13
  503. package/src/modules/access/access.routes.ts +34 -15
  504. package/src/modules/access/admin-locked-commit.ts +11 -11
  505. package/src/modules/access/admin-route-helpers.ts +5 -5
  506. package/src/modules/access/change-read-gate.ts +1 -1
  507. package/src/modules/access/groups-admin.service.ts +7 -7
  508. package/src/modules/access/index.ts +1 -1
  509. package/src/modules/access/reference-scan.ts +1 -1
  510. package/src/modules/access/roles-admin.service.ts +9 -9
  511. package/src/modules/access/synced-groups-committer.ts +11 -11
  512. package/src/modules/access/user-access-removal.service.ts +7 -7
  513. package/src/modules/access-model/__tests__/access-grammar.test.ts +2 -2
  514. package/src/modules/access-model/access-errors.ts +4 -4
  515. package/src/modules/access-model/access-grammar.ts +4 -4
  516. package/src/modules/access-model/change-gate.ts +1 -1
  517. package/src/modules/access-model/roles-yaml-guard.ts +4 -4
  518. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +98 -29
  519. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +39 -12
  520. package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
  521. package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
  522. package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
  523. package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
  524. package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
  525. package/src/modules/agent-guide/agent-guide.tools.ts +49 -31
  526. package/src/modules/agent-guide/agent-guide.ts +165 -32
  527. package/src/modules/agent-guide/guide-budget.ts +77 -0
  528. package/src/modules/agent-guide/index.ts +12 -0
  529. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +4 -5
  530. package/src/modules/agent-instructions/__tests__/compose.test.ts +1 -1
  531. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +117 -11
  532. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -5
  533. package/src/modules/agent-instructions/compose.ts +28 -9
  534. package/src/modules/agent-instructions/shared-file-rules.ts +62 -18
  535. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +1 -1
  536. package/src/modules/audit/__tests__/agent-rest-audit.middleware.test.ts +113 -113
  537. package/src/modules/audit/__tests__/event-classifier.test.ts +121 -121
  538. package/src/modules/audit/event-classifier.ts +155 -155
  539. package/src/modules/auth/__tests__/account-deactivation.test.ts +1 -1
  540. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +4 -4
  541. package/src/modules/auth/__tests__/account-erasure.participants.test.ts +56 -0
  542. package/src/modules/auth/__tests__/account-switch-lock.pg.test.ts +2 -2
  543. package/src/modules/auth/__tests__/account-switch-lock.test.ts +2 -2
  544. package/src/modules/auth/__tests__/account.routes.test.ts +73 -11
  545. package/src/modules/auth/__tests__/auth.service.test.ts +1 -1
  546. package/src/modules/auth/account-erasure.service.ts +27 -9
  547. package/src/modules/auth/account-hooks.ts +34 -0
  548. package/src/modules/auth/account-switch-lock.ts +2 -2
  549. package/src/modules/auth/account.routes.ts +23 -5
  550. package/src/modules/auth/auth.service.ts +4 -4
  551. package/src/modules/auth/index.ts +1 -0
  552. package/src/modules/code-mode/code-mode.tool.ts +2 -2
  553. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
  554. package/src/modules/connection-probe/connection-probe.service.ts +1 -34
  555. package/src/modules/database/__tests__/advisory-lock.test.ts +169 -169
  556. package/src/modules/database/__tests__/migrations-unqualified.test.ts +32 -32
  557. package/src/modules/database/core-schema.ts +2 -2
  558. package/src/modules/diff/__tests__/diff.routes.read-gate.test.ts +2 -2
  559. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +12 -12
  560. package/src/modules/diff/diff.routes.ts +8 -8
  561. package/src/modules/embed/__tests__/embed.service.lock.test.ts +7 -7
  562. package/src/modules/embed/__tests__/embed.service.test.ts +42 -42
  563. package/src/modules/embed/__tests__/embed.tools.test.ts +1 -1
  564. package/src/modules/embed/embed.routes.ts +1 -1
  565. package/src/modules/embed/embed.service.ts +17 -17
  566. package/src/modules/kb-fs/__tests__/branch-name.test.ts +2 -2
  567. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +260 -260
  568. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +34 -34
  569. package/src/modules/kb-fs/__tests__/repo-path.test.ts +8 -8
  570. package/src/modules/kb-fs/branch-name.ts +8 -8
  571. package/src/modules/kb-fs/file-change-notifier.ts +2 -2
  572. package/src/modules/kb-fs/locking-filesystem.ts +30 -30
  573. package/src/modules/kb-fs/repo-path.ts +5 -5
  574. package/src/modules/kb-fs/tmp-ref.ts +3 -3
  575. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +36 -36
  576. package/src/modules/kb-sync/index.ts +1 -1
  577. package/src/modules/kb-sync/kb-sync.interface.ts +3 -3
  578. package/src/modules/kb-sync/kb-sync.service.ts +9 -9
  579. package/src/modules/marketplace/__tests__/github-facade.test.ts +1 -1
  580. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +1 -1
  581. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +4 -2
  582. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +69 -8
  583. package/src/modules/mcp/__tests__/mcp.service.test.ts +91 -13
  584. package/src/modules/mcp/__tests__/read-only-hint.pg.test.ts +194 -0
  585. package/src/modules/mcp/mcp.service.ts +93 -62
  586. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +55 -55
  587. package/src/modules/onboarding/onboarding.contract.ts +1 -1
  588. package/src/modules/onboarding/onboarding.routes.ts +2 -2
  589. package/src/modules/onboarding/starter-pack.service.ts +6 -6
  590. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +1 -1
  591. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +1 -1
  592. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +43 -43
  593. package/src/modules/plugins/__tests__/join-requests.service.test.ts +19 -19
  594. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +26 -26
  595. package/src/modules/plugins/__tests__/plugins.routes.test.ts +25 -25
  596. package/src/modules/plugins/join-request-jobs.service.ts +10 -10
  597. package/src/modules/plugins/join-requests.service.ts +4 -4
  598. package/src/modules/plugins/plugin-rename.service.ts +1 -1
  599. package/src/modules/plugins/plugins.routes.ts +9 -9
  600. package/src/modules/secrets-vault/__tests__/db-secrets-vault.managed.test.ts +773 -0
  601. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +458 -4
  602. package/src/modules/secrets-vault/__tests__/managed-sign-in.route.test.ts +351 -0
  603. package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
  604. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
  605. package/src/modules/secrets-vault/db-secrets-vault.service.ts +728 -118
  606. package/src/modules/secrets-vault/index.ts +8 -1
  607. package/src/modules/secrets-vault/managed-sign-in-erasure.ts +41 -0
  608. package/src/modules/secrets-vault/secrets-variable-loader.ts +134 -134
  609. package/src/modules/secrets-vault/secrets-vault.contract.ts +213 -11
  610. package/src/modules/secrets-vault/secrets-vault.routes.ts +244 -45
  611. package/src/modules/settings/__tests__/repository-source.test.ts +1 -1
  612. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +1 -1
  613. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +1 -1
  614. package/src/modules/settings/connection-check.ts +1 -1
  615. package/src/modules/settings/deployment-settings.service.ts +1 -1
  616. package/src/modules/settings/git-root-folders.ts +1 -1
  617. package/src/modules/settings/oidc-check.ts +5 -18
  618. package/src/modules/settings/setup.routes.ts +1 -1
  619. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +3 -3
  620. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
  621. package/src/modules/skills/__tests__/pending-skills.service.test.ts +8 -8
  622. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +18 -18
  623. package/src/modules/skills/__tests__/skills.service.test.ts +3 -3
  624. package/src/modules/skills/pending-skills.service.ts +3 -3
  625. package/src/modules/skills/skill-access-requests.routes.ts +7 -7
  626. package/src/modules/skills/skills.tools.ts +1 -1
  627. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +22 -14
  628. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +3 -3
  629. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -3
  630. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +4 -4
  631. package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
  632. package/src/modules/tool-helpers/__tests__/tools-surface.test.ts +3 -3
  633. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +1 -1
  634. package/src/modules/tool-helpers/tool-context.ts +6 -6
  635. package/src/modules/tool-helpers/tool-def.ts +1 -1
  636. package/src/modules/tool-helpers/tool-handler.ts +2 -2
  637. package/src/modules/tool-helpers/tool.contract.ts +4 -4
  638. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +9 -9
  639. package/src/modules/tool-manuals/__tests__/tool-manuals.managed-sign-in.test.ts +89 -0
  640. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
  641. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +1 -1
  642. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +132 -6
  643. package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
  644. package/src/modules/tool-manuals/mcp-server-edit.service.ts +1 -1
  645. package/src/modules/tool-manuals/pending-tools.service.ts +3 -3
  646. package/src/modules/tool-manuals/tool-delete.service.ts +1 -1
  647. package/src/modules/tool-manuals/tool-manuals.contract.ts +63 -3
  648. package/src/modules/tool-manuals/tool-manuals.service.ts +191 -51
  649. package/src/modules/tool-manuals/tool-manuals.tools.ts +93 -28
  650. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +4 -4
  651. package/src/modules/tool-registry/__tests__/read-only-hints.test.ts +3 -3
  652. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +12 -119
  653. package/src/modules/{workflow → version-control}/__tests__/apply-failure.test.ts +54 -54
  654. package/src/modules/{workflow → version-control}/__tests__/event-bus.test.ts +27 -27
  655. package/src/modules/{workflow → version-control}/__tests__/events-delivery.test.ts +37 -37
  656. package/src/modules/{workflow → version-control}/__tests__/file-lock.service.deletion-gate.test.ts +2 -2
  657. package/src/modules/{workflow → version-control}/__tests__/file-lock.service.path-identity.test.ts +8 -8
  658. package/src/modules/{workflow → version-control}/__tests__/merge-announces-tree.test.ts +11 -11
  659. package/src/modules/{workflow → version-control}/__tests__/pending-commits.worker.concurrency.test.ts +9 -9
  660. package/src/modules/{workflow → version-control}/__tests__/pending-commits.worker.test.ts +25 -25
  661. package/src/modules/{workflow → version-control}/__tests__/preserve-roles-yaml.test.ts +42 -42
  662. package/src/modules/version-control/__tests__/public-surface.test.ts +114 -0
  663. package/src/modules/{workflow/__tests__/workflow.routes.folder-proposals.test.ts → version-control/__tests__/version-control.routes.folder-proposals.test.ts} +18 -18
  664. package/src/modules/{workflow/__tests__/workflow.routes.fork-point-file.test.ts → version-control/__tests__/version-control.routes.fork-point-file.test.ts} +19 -19
  665. package/src/modules/{workflow/__tests__/workflow.routes.history-read-gate.test.ts → version-control/__tests__/version-control.routes.history-read-gate.test.ts} +13 -13
  666. package/src/modules/{workflow/__tests__/workflow.routes.lock-path.test.ts → version-control/__tests__/version-control.routes.lock-path.test.ts} +18 -18
  667. package/src/modules/{workflow/__tests__/workflow.routes.open-change-request.test.ts → version-control/__tests__/version-control.routes.open-change-request.test.ts} +7 -7
  668. package/src/modules/{workflow/__tests__/workflow.routes.push-refused.test.ts → version-control/__tests__/version-control.routes.push-refused.test.ts} +14 -14
  669. package/src/modules/{workflow/__tests__/workflow.service.branch-in-use.test.ts → version-control/__tests__/version-control.service.branch-in-use.test.ts} +6 -6
  670. package/src/modules/{workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts → version-control/__tests__/version-control.service.commitFileWhileLocked.test.ts} +14 -14
  671. package/src/modules/{workflow/__tests__/workflow.service.delete-branch.test.ts → version-control/__tests__/version-control.service.delete-branch.test.ts} +14 -14
  672. package/src/modules/{workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts → version-control/__tests__/version-control.service.deleted-branch-sweep.test.ts} +4 -4
  673. package/src/modules/{workflow/__tests__/workflow.service.facade.test.ts → version-control/__tests__/version-control.service.facade.test.ts} +97 -99
  674. package/src/modules/{workflow/__tests__/workflow.service.folder-proposals.test.ts → version-control/__tests__/version-control.service.folder-proposals.test.ts} +7 -7
  675. package/src/modules/{workflow/__tests__/workflow.service.platform-restore.test.ts → version-control/__tests__/version-control.service.platform-restore.test.ts} +6 -6
  676. package/src/modules/{workflow/__tests__/workflow.service.read-gate.test.ts → version-control/__tests__/version-control.service.read-gate.test.ts} +9 -9
  677. package/src/modules/{workflow/__tests__/workflow.service.releaseLock.test.ts → version-control/__tests__/version-control.service.releaseLock.test.ts} +42 -42
  678. package/src/modules/{workflow/__tests__/workflow.service.repository-replaced.test.ts → version-control/__tests__/version-control.service.repository-replaced.test.ts} +4 -4
  679. package/src/modules/{workflow/__tests__/workflow.service.sync.test.ts → version-control/__tests__/version-control.service.sync.test.ts} +17 -17
  680. package/src/modules/{workflow/__tests__/workflow.service.update-change-request.pg.test.ts → version-control/__tests__/version-control.service.update-change-request.pg.test.ts} +6 -6
  681. package/src/modules/{workflow/__tests__/workflow.service.update-change-request.test.ts → version-control/__tests__/version-control.service.update-change-request.test.ts} +11 -11
  682. package/src/modules/{workflow/__tests__/workflow.service.update-from-target.test.ts → version-control/__tests__/version-control.service.update-from-target.test.ts} +22 -22
  683. package/src/modules/{workflow → version-control}/agent-tools/__tests__/change-request-read.tools.test.ts +3 -3
  684. package/src/modules/{workflow → version-control}/agent-tools/__tests__/update-change-request.parity.test.ts +16 -16
  685. package/src/modules/{workflow/agent-tools/__tests__/workflow.tools.test.ts → version-control/agent-tools/__tests__/version-control.tools.test.ts} +9 -9
  686. package/src/modules/{workflow → version-control}/agent-tools/change-request-read.tools.ts +2 -2
  687. package/src/modules/{workflow/agent-tools/workflow.tools.ts → version-control/agent-tools/version-control.tools.ts} +52 -48
  688. package/src/modules/{workflow → version-control}/change-request-hooks.ts +1 -1
  689. package/src/modules/{workflow/review-workflow → version-control/change-request-review}/__tests__/approval-states.test.ts +5 -5
  690. package/src/modules/{workflow/review-workflow → version-control/change-request-review}/__tests__/cancel-pr.test.ts +8 -8
  691. package/src/modules/{workflow/review-workflow → version-control/change-request-review}/__tests__/carry-approvals-forward.test.ts +6 -6
  692. package/src/modules/{workflow/review-workflow → version-control/change-request-review}/__tests__/erase-approver.test.ts +5 -5
  693. package/src/modules/{workflow/review-workflow → version-control/change-request-review}/__tests__/merge-gate.test.ts +3 -3
  694. package/src/modules/{workflow/review-workflow → version-control/change-request-review}/__tests__/merge-records-own-commit.test.ts +3 -3
  695. package/src/modules/{workflow/review-workflow/review-workflow.interface.ts → version-control/change-request-review/change-request-review.interface.ts} +1 -1
  696. package/src/modules/{workflow/review-workflow/review-workflow.service.ts → version-control/change-request-review/change-request-review.service.ts} +28 -28
  697. package/src/modules/{workflow → version-control}/event-bus.ts +14 -14
  698. package/src/modules/{workflow → version-control}/events-delivery.ts +11 -11
  699. package/src/modules/{workflow → version-control}/events.routes.ts +5 -5
  700. package/src/modules/{workflow → version-control}/file-lock.service.ts +6 -6
  701. package/src/modules/{workflow → version-control}/git/__tests__/git-test-helpers.ts +3 -3
  702. package/src/modules/{workflow → version-control}/git/__tests__/git.service.accessGating.test.ts +5 -5
  703. package/src/modules/{workflow → version-control}/git/__tests__/git.service.appliedChange.test.ts +4 -4
  704. package/src/modules/{workflow → version-control}/git/__tests__/git.service.changedFilesForPr.test.ts +16 -16
  705. package/src/modules/{workflow → version-control}/git/__tests__/git.service.changedPathsMemo.test.ts +2 -2
  706. package/src/modules/{workflow → version-control}/git/__tests__/git.service.commitChanges.test.ts +3 -3
  707. package/src/modules/{workflow → version-control}/git/__tests__/git.service.commitFile.strayPath.test.ts +10 -10
  708. package/src/modules/{workflow → version-control}/git/__tests__/git.service.commitFile.test.ts +3 -3
  709. package/src/modules/{workflow → version-control}/git/__tests__/git.service.createBranch.test.ts +3 -3
  710. package/src/modules/{workflow → version-control}/git/__tests__/git.service.deleteBranch.test.ts +7 -7
  711. package/src/modules/{workflow → version-control}/git/__tests__/git.service.diffFileAtCommit.test.ts +9 -9
  712. package/src/modules/{workflow → version-control}/git/__tests__/git.service.diffFileBetweenBranches.test.ts +23 -23
  713. package/src/modules/{workflow → version-control}/git/__tests__/git.service.fileBytesAtCommit.test.ts +7 -7
  714. package/src/modules/{workflow → version-control}/git/__tests__/git.service.fileContentsAtCommit.test.ts +3 -3
  715. package/src/modules/{workflow → version-control}/git/__tests__/git.service.forkPoint.test.ts +2 -2
  716. package/src/modules/{workflow → version-control}/git/__tests__/git.service.history-guards.test.ts +2 -2
  717. package/src/modules/{workflow → version-control}/git/__tests__/git.service.list-branches.test.ts +12 -12
  718. package/src/modules/{workflow → version-control}/git/__tests__/git.service.listTrackedFiles.test.ts +2 -2
  719. package/src/modules/{workflow → version-control}/git/__tests__/git.service.logForFile.test.ts +6 -6
  720. package/src/modules/{workflow → version-control}/git/__tests__/git.service.mergeChangeRequest.test.ts +20 -20
  721. package/src/modules/{workflow → version-control}/git/__tests__/git.service.mergeIntoBranch.test.ts +16 -16
  722. package/src/modules/{workflow → version-control}/git/__tests__/git.service.pendingChanges.test.ts +2 -2
  723. package/src/modules/{workflow → version-control}/git/__tests__/git.service.postMergePull.test.ts +5 -5
  724. package/src/modules/{workflow → version-control}/git/__tests__/git.service.prFetchFailure.test.ts +2 -2
  725. package/src/modules/{workflow → version-control}/git/__tests__/git.service.pull.test.ts +12 -12
  726. package/src/modules/{workflow → version-control}/git/__tests__/git.service.readFileAtRef.test.ts +3 -3
  727. package/src/modules/{workflow → version-control}/git/__tests__/git.service.revertPathsAndPush.test.ts +2 -2
  728. package/src/modules/{workflow → version-control}/git/__tests__/git.service.status.test.ts +4 -4
  729. package/src/modules/{workflow → version-control}/git/__tests__/git.service.syncFromRemote.test.ts +8 -8
  730. package/src/modules/{workflow → version-control}/git/__tests__/git.service.worktrees.test.ts +4 -4
  731. package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.list-fetch.test.ts +2 -2
  732. package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.test.ts +6 -6
  733. package/src/modules/{workflow → version-control}/git/git.service.ts +57 -57
  734. package/src/modules/{workflow → version-control}/git/node-git-runner.ts +1 -1
  735. package/src/modules/{workflow → version-control}/git/pull-request.service.ts +12 -12
  736. package/src/modules/{workflow → version-control}/index.ts +6 -5
  737. package/src/modules/{workflow → version-control}/pending-commits.service.ts +2 -2
  738. package/src/modules/{workflow → version-control}/pending-commits.worker.ts +8 -8
  739. package/src/modules/{workflow → version-control}/read-on-branch.ts +2 -2
  740. package/src/modules/{workflow/workflow-hooks.ts → version-control/version-control-hooks.ts} +3 -3
  741. package/src/modules/{workflow/workflow.routes.ts → version-control/version-control.routes.ts} +64 -63
  742. package/src/modules/{workflow/workflow.service.ts → version-control/version-control.service.ts} +92 -97
  743. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +8 -8
  744. package/src/modules/workspace/__tests__/agent-downloads.test.ts +13 -16
  745. package/src/modules/workspace/__tests__/agent-uploads.test.ts +11 -12
  746. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +11 -16
  747. package/src/modules/workspace/__tests__/file-stat-access.test.ts +3 -3
  748. package/src/modules/workspace/__tests__/git-internals.security.test.ts +27 -27
  749. package/src/modules/workspace/__tests__/kb-repository.test.ts +1 -1
  750. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  751. package/src/modules/workspace/__tests__/start-session.default-branch.test.ts +152 -0
  752. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +6 -6
  753. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +13 -14
  754. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +6 -6
  755. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +4 -4
  756. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +6 -6
  757. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +8 -8
  758. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +4 -4
  759. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +6 -6
  760. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +6 -6
  761. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +4 -4
  762. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +6 -6
  763. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +7 -7
  764. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +13 -13
  765. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +1 -1
  766. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +1 -1
  767. package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +1 -1
  768. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +12 -14
  769. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +5 -5
  770. package/src/modules/workspace/__tests__/workspace.tools.test.ts +182 -75
  771. package/src/modules/workspace/agent-access.gate.ts +6 -6
  772. package/src/modules/workspace/git-internals.middleware.ts +2 -2
  773. package/src/modules/workspace/kb-repository.ts +1 -1
  774. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +1 -1
  775. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +1 -1
  776. package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +1 -1
  777. package/src/modules/workspace/startup/kb-git.ts +1 -1
  778. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +94 -70
  779. package/src/modules/workspace/startup/steps/seed-tree.ts +4 -5
  780. package/src/modules/workspace/startup/steps/template-files.step.ts +4 -5
  781. package/src/modules/workspace/startup/steps/template-source.ts +21 -6
  782. package/src/modules/workspace/workspace-tmp.ts +4 -4
  783. package/src/modules/workspace/workspace.routes.ts +26 -26
  784. package/src/modules/workspace/workspace.service.ts +11 -11
  785. package/src/modules/workspace/workspace.tools.ts +90 -30
  786. package/src/shared/__tests__/canonical-file-identity.test.ts +5 -5
  787. package/src/shared/__tests__/http-errors.test.ts +4 -4
  788. package/src/shared/__tests__/read-capped.test.ts +68 -0
  789. package/src/shared/canonical-file-identity.ts +5 -5
  790. package/src/shared/domain-errors.ts +44 -44
  791. package/src/shared/git.contract.ts +1 -1
  792. package/src/shared/http-errors.ts +3 -3
  793. package/src/shared/kb-context.ts +130 -130
  794. package/src/shared/oauth-token-error.ts +46 -0
  795. package/src/shared/pending-proposals.ts +4 -4
  796. package/src/shared/read-capped.ts +40 -0
  797. package/src/tenancy/__tests__/tenant-host.test.ts +372 -372
  798. package/src/tenancy/__tests__/tenant-runtime.test.ts +221 -221
  799. package/src/tenancy/__tests__/tenant-secrets.test.ts +35 -35
  800. package/src/tenancy/index.ts +35 -35
  801. package/src/tenancy/static-tenant-source.ts +1 -1
  802. package/src/tenancy/tenant-host.ts +261 -261
  803. package/src/tenancy/tenant-runtime.ts +208 -208
  804. package/src/tenancy/tenant-source.contract.ts +59 -59
  805. package/agent-guide/finding-things.md +0 -7
  806. package/agent-guide/introduction.md +0 -27
  807. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +0 -1
  808. package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +0 -1
  809. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +0 -1
  810. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +0 -1
  811. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +0 -1
  812. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +0 -1
  813. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +0 -1
  814. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +0 -1
  815. package/dist/modules/workflow/change-request-hooks.d.ts.map +0 -1
  816. package/dist/modules/workflow/change-request-hooks.js.map +0 -1
  817. package/dist/modules/workflow/event-bus.d.ts.map +0 -1
  818. package/dist/modules/workflow/event-bus.js.map +0 -1
  819. package/dist/modules/workflow/events-delivery.d.ts.map +0 -1
  820. package/dist/modules/workflow/events-delivery.js.map +0 -1
  821. package/dist/modules/workflow/events.routes.d.ts.map +0 -1
  822. package/dist/modules/workflow/events.routes.js.map +0 -1
  823. package/dist/modules/workflow/file-lock.service.d.ts.map +0 -1
  824. package/dist/modules/workflow/file-lock.service.js.map +0 -1
  825. package/dist/modules/workflow/git/change-request-link.d.ts.map +0 -1
  826. package/dist/modules/workflow/git/change-request-link.js.map +0 -1
  827. package/dist/modules/workflow/git/git-version.d.ts.map +0 -1
  828. package/dist/modules/workflow/git/git-version.js.map +0 -1
  829. package/dist/modules/workflow/git/git.service.d.ts.map +0 -1
  830. package/dist/modules/workflow/git/git.service.js.map +0 -1
  831. package/dist/modules/workflow/git/merge-commit.d.ts.map +0 -1
  832. package/dist/modules/workflow/git/merge-commit.js.map +0 -1
  833. package/dist/modules/workflow/git/node-git-runner.d.ts.map +0 -1
  834. package/dist/modules/workflow/git/node-git-runner.js.map +0 -1
  835. package/dist/modules/workflow/git/pull-request.service.d.ts.map +0 -1
  836. package/dist/modules/workflow/git/pull-request.service.js.map +0 -1
  837. package/dist/modules/workflow/index.d.ts.map +0 -1
  838. package/dist/modules/workflow/index.js.map +0 -1
  839. package/dist/modules/workflow/pending-commits.service.d.ts.map +0 -1
  840. package/dist/modules/workflow/pending-commits.service.js.map +0 -1
  841. package/dist/modules/workflow/pending-commits.worker.d.ts.map +0 -1
  842. package/dist/modules/workflow/pending-commits.worker.js.map +0 -1
  843. package/dist/modules/workflow/read-on-branch.d.ts.map +0 -1
  844. package/dist/modules/workflow/read-on-branch.js.map +0 -1
  845. package/dist/modules/workflow/recovery-bot.d.ts.map +0 -1
  846. package/dist/modules/workflow/recovery-bot.js.map +0 -1
  847. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +0 -1
  848. package/dist/modules/workflow/review-workflow/approval-lock.js.map +0 -1
  849. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +0 -1
  850. package/dist/modules/workflow/review-workflow/review-workflow.interface.js +0 -2
  851. package/dist/modules/workflow/review-workflow/review-workflow.interface.js.map +0 -1
  852. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +0 -1
  853. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +0 -1
  854. package/dist/modules/workflow/sanitize-error.d.ts.map +0 -1
  855. package/dist/modules/workflow/sanitize-error.js.map +0 -1
  856. package/dist/modules/workflow/workflow-hooks.d.ts.map +0 -1
  857. package/dist/modules/workflow/workflow-hooks.js.map +0 -1
  858. package/dist/modules/workflow/workflow.routes.d.ts.map +0 -1
  859. package/dist/modules/workflow/workflow.routes.js.map +0 -1
  860. package/dist/modules/workflow/workflow.service.d.ts.map +0 -1
  861. package/dist/modules/workflow/workflow.service.js.map +0 -1
  862. package/src/modules/mcp/__tests__/read-only-hint.e2e.test.ts +0 -213
  863. /package/dist/modules/{workflow → version-control}/agent-tools/change-request-read-shape.d.ts +0 -0
  864. /package/dist/modules/{workflow → version-control}/agent-tools/change-request-read-shape.js +0 -0
  865. /package/dist/modules/{workflow → version-control}/agent-tools/change-request-read.tools.d.ts +0 -0
  866. /package/dist/modules/{workflow → version-control}/agent-tools/change-request-summary.d.ts +0 -0
  867. /package/dist/modules/{workflow → version-control}/agent-tools/change-request-summary.js +0 -0
  868. /package/dist/modules/{workflow → version-control}/change-request-hooks.js +0 -0
  869. /package/dist/modules/{workflow/review-workflow → version-control/change-request-review}/approval-lock.d.ts +0 -0
  870. /package/dist/modules/{workflow/review-workflow → version-control/change-request-review}/approval-lock.js +0 -0
  871. /package/dist/modules/{workflow → version-control}/events-delivery.js +0 -0
  872. /package/dist/modules/{workflow → version-control}/events.routes.js +0 -0
  873. /package/dist/modules/{workflow → version-control}/git/change-request-link.d.ts +0 -0
  874. /package/dist/modules/{workflow → version-control}/git/change-request-link.js +0 -0
  875. /package/dist/modules/{workflow → version-control}/git/git-version.d.ts +0 -0
  876. /package/dist/modules/{workflow → version-control}/git/git-version.js +0 -0
  877. /package/dist/modules/{workflow → version-control}/git/merge-commit.d.ts +0 -0
  878. /package/dist/modules/{workflow → version-control}/git/merge-commit.js +0 -0
  879. /package/dist/modules/{workflow → version-control}/git/node-git-runner.d.ts +0 -0
  880. /package/dist/modules/{workflow → version-control}/recovery-bot.d.ts +0 -0
  881. /package/dist/modules/{workflow → version-control}/recovery-bot.js +0 -0
  882. /package/dist/modules/{workflow → version-control}/sanitize-error.d.ts +0 -0
  883. /package/dist/modules/{workflow → version-control}/sanitize-error.js +0 -0
  884. /package/src/modules/{workflow → version-control}/__tests__/change-request-hooks.test.ts +0 -0
  885. /package/src/modules/{workflow → version-control}/__tests__/fake-file-lock-db.ts +0 -0
  886. /package/src/modules/{workflow → version-control}/__tests__/pending-commits.canonical.test.ts +0 -0
  887. /package/src/modules/{workflow → version-control}/__tests__/pending-commits.enqueue-if-absent.test.ts +0 -0
  888. /package/src/modules/{workflow → version-control}/__tests__/pending-commits.repository-replaced.test.ts +0 -0
  889. /package/src/modules/{workflow → version-control}/__tests__/sanitize-error.test.ts +0 -0
  890. /package/src/modules/{workflow → version-control}/agent-tools/__tests__/change-request-read-shape.test.ts +0 -0
  891. /package/src/modules/{workflow → version-control}/agent-tools/change-request-read-shape.ts +0 -0
  892. /package/src/modules/{workflow → version-control}/agent-tools/change-request-summary.ts +0 -0
  893. /package/src/modules/{workflow/review-workflow → version-control/change-request-review}/approval-lock.ts +0 -0
  894. /package/src/modules/{workflow → version-control}/git/__tests__/branchAuthor.test.ts +0 -0
  895. /package/src/modules/{workflow → version-control}/git/__tests__/change-request-link.test.ts +0 -0
  896. /package/src/modules/{workflow → version-control}/git/__tests__/git-version.failure.test.ts +0 -0
  897. /package/src/modules/{workflow → version-control}/git/__tests__/git-version.test.ts +0 -0
  898. /package/src/modules/{workflow → version-control}/git/__tests__/node-git-runner.test.ts +0 -0
  899. /package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.getPrDetail.test.ts +0 -0
  900. /package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.placeholder.test.ts +0 -0
  901. /package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +0 -0
  902. /package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.viewer-can-delete.test.ts +0 -0
  903. /package/src/modules/{workflow → version-control}/git/__tests__/pull-request.service.viewer-can-update.test.ts +0 -0
  904. /package/src/modules/{workflow → version-control}/git/change-request-link.ts +0 -0
  905. /package/src/modules/{workflow → version-control}/git/git-version.ts +0 -0
  906. /package/src/modules/{workflow → version-control}/git/merge-commit.ts +0 -0
  907. /package/src/modules/{workflow → version-control}/recovery-bot.ts +0 -0
  908. /package/src/modules/{workflow → version-control}/sanitize-error.ts +0 -0
@@ -1,12 +1,15 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
+ import { isDeepStrictEqual } from 'node:util';
2
3
  import { logger } from '../../shared/logging.js';
3
4
 
4
5
  const log = logger('vault');
5
6
  import { and, desc, eq, inArray, isNull, like, or } from 'drizzle-orm';
6
7
  import type { Database } from '../database/index.js';
7
- import { secrets } from '../database/index.js';
8
+ import { pgErrorCode, secrets } from '../database/index.js';
8
9
  import { TokenCrypto } from '../../shared/token-crypto.js';
9
10
  import { assertSafeFetchUrl } from '../../shared/ssrf.js';
11
+ import { readCapped } from '../../shared/read-capped.js';
12
+ import { standardTokenErrorCode } from '../../shared/oauth-token-error.js';
10
13
  import {
11
14
  type ISecretsVaultService,
12
15
  type SecretSummary,
@@ -16,14 +19,21 @@ import {
16
19
  type PutSharedStaticSecretInput,
17
20
  type CreateOAuthSecretInput,
18
21
  type OAuthProviderConfig,
22
+ type DeclaredOAuthProvider,
19
23
  type VariableScopeResolver,
20
24
  type NamespaceSecretCount,
21
25
  isKeyInNamespace,
22
26
  type ForcedRefreshOutcome,
27
+ type ManagedSignInRef,
28
+ type ManagedTokenRefresher,
29
+ type ManagedTokenRevoker,
30
+ type DeliveredTokenSet,
31
+ type ManagedRowsWriter,
23
32
  InvalidSecretError,
24
33
  SecretNotFoundError,
25
34
  SecretOAuthError,
26
35
  } from './secrets-vault.contract.js';
36
+ import { timingSafeStringEqual } from '../auth/index.js';
27
37
 
28
38
  const MAX_KEY_LEN = 200;
29
39
  const MAX_LABEL_LEN = 200;
@@ -32,6 +42,104 @@ const MAX_VALUE_LEN = 100_000;
32
42
  const REFRESH_SKEW_MS = 60_000;
33
43
  /** Upper bound on a token-endpoint round-trip, so a hung provider can't block callers. */
34
44
  const TOKEN_REQUEST_TIMEOUT_MS = 10_000;
45
+ /** Upper bound on telling a provider a managed sign-in is over, so a deletion never waits on it. */
46
+ const REVOKE_TIMEOUT_MS = 10_000;
47
+ /** Upper bound on a broker renewing a managed token: past it the stale token is served and the next call asks again. */
48
+ const REFRESH_TIMEOUT_MS = 15_000;
49
+ /** How many times a guarded replacement re-reads a row that changed under its revoke before giving up. */
50
+ const REPLACE_ATTEMPTS = 3;
51
+ /** How many times a handoff re-reads a row a refresh rotated under it (the nonce rides along) before refusing. */
52
+ const HANDOFF_ATTEMPTS = 3;
53
+
54
+ /** `work`, or a rejection once `ms` have passed: a handler that never settles must not hold the vault. */
55
+ async function bounded<T>(work: Promise<T>, ms: number, what: string): Promise<T> {
56
+ let timer: ReturnType<typeof setTimeout> | undefined;
57
+ try {
58
+ return await Promise.race([
59
+ work,
60
+ new Promise<never>((_, reject) => {
61
+ timer = setTimeout(() => reject(new Error(`${what} did not settle within ${ms}ms`)), ms);
62
+ }),
63
+ ]);
64
+ } finally {
65
+ if (timer) clearTimeout(timer);
66
+ }
67
+ }
68
+ /** How often a managed sign-in's opening is retried against a row that keeps changing under it. */
69
+ const BEGIN_MANAGED_ATTEMPTS = 3;
70
+ /** How much of a refusing token endpoint's body is read for its error code: an OAuth error object, never a whole error page. */
71
+ const TOKEN_ERROR_BODY_MAX_BYTES = 2048;
72
+ // Hexis's own sentences for a refused token request: what to change, in our
73
+ // words, never the provider's.
74
+ const HINT_CLIENT =
75
+ 'The client id or client secret this sign-in uses is not accepted by the provider. The tool owner should check the declared client id and set the client secret again.';
76
+ const HINT_GRANT =
77
+ 'The provider refused the grant: for a sign-in, a redirect URL mismatch or an expired or reused authorization code; for a refresh, a refresh token that has expired or been revoked. Sign in again.';
78
+ const HINT_SCOPE =
79
+ "The provider refused the scopes this sign-in asks for. The tool owner should check the declared scopes against the provider's app.";
80
+ const HINT_UNAUTHORIZED =
81
+ "The provider does not allow this client to use this grant. The tool owner should check the provider's app settings.";
82
+ const HINT_REQUEST =
83
+ 'The provider found the token request malformed. The tool owner should check the declared endpoints and parameters.';
84
+ const HINT_PROVIDER = 'The provider could not serve the token request. Try again later.';
85
+
86
+ /**
87
+ * Hexis's sentence for each error code a refusing token endpoint may answer:
88
+ * the standard ones (`shared/oauth-token-error.ts` is the one list of those,
89
+ * read here and by the OIDC configuration check) in `error`, and the ones
90
+ * HubSpot's token endpoint answers in its `status` field. Only a code from
91
+ * those lists is ever taken, exactly, so no other string a provider puts in a
92
+ * field — a secret, a secret with a suffix, free text — can reach a message.
93
+ * Nothing else of the body is ever taken: no description, no message, no first
94
+ * line. That is why a message built here needs no scrubbing, cutting or
95
+ * decoding, and why every spelling a secret could reach a log in (escaped,
96
+ * encoded, cut) stays inexpressible. A standard code without a sentence here
97
+ * (`access_denied`) is named and left unexplained.
98
+ */
99
+ const TOKEN_ERROR_HINTS: ReadonlyMap<string, string> = new Map([
100
+ ['invalid_request', HINT_REQUEST],
101
+ ['invalid_client', HINT_CLIENT],
102
+ ['invalid_grant', HINT_GRANT],
103
+ ['unauthorized_client', HINT_UNAUTHORIZED],
104
+ ['unsupported_grant_type', HINT_UNAUTHORIZED],
105
+ ['invalid_scope', HINT_SCOPE],
106
+ ['server_error', HINT_PROVIDER],
107
+ ['temporarily_unavailable', HINT_PROVIDER],
108
+ ['BAD_CLIENT_ID', HINT_CLIENT],
109
+ ['BAD_CLIENT_SECRET', HINT_CLIENT],
110
+ ['BAD_AUTH_CODE', HINT_GRANT],
111
+ ['BAD_REDIRECT_URI', HINT_GRANT],
112
+ ]);
113
+
114
+ /** The codes HubSpot's token endpoint answers in `status` instead of a standard `error`. */
115
+ const HUBSPOT_STATUS_CODES: ReadonlySet<string> = new Set(['BAD_CLIENT_ID', 'BAD_CLIENT_SECRET', 'BAD_AUTH_CODE', 'BAD_REDIRECT_URI']);
116
+
117
+ /**
118
+ * The known error code a refusing token endpoint answered, or null: a standard
119
+ * `error` (RFC 6749 §5.2), else HubSpot's own `status`, when it is exactly one
120
+ * of the codes known. Without even the code, a bare "HTTP 400" cannot tell a
121
+ * client the provider does not accept from a refused grant.
122
+ */
123
+ export function tokenEndpointErrorCode(bodyText: string): string | null {
124
+ const standard = standardTokenErrorCode(bodyText);
125
+ if (standard !== null) return standard;
126
+ let json: unknown;
127
+ try {
128
+ json = JSON.parse(bodyText);
129
+ } catch {
130
+ return null;
131
+ }
132
+ if (!json || typeof json !== 'object' || Array.isArray(json)) return null;
133
+ const { status } = json as Record<string, unknown>;
134
+ return typeof status === 'string' && HUBSPOT_STATUS_CODES.has(status) ? status : null;
135
+ }
136
+
137
+ /** The refusal in one message: the status, the known code when there is one, and Hexis's sentence for it. */
138
+ export function describeTokenEndpointRefusal(status: number, code: string | null): string {
139
+ const base = `Token endpoint returned HTTP ${status}`;
140
+ const hint = code === null ? undefined : TOKEN_ERROR_HINTS.get(code);
141
+ return hint ? `${base} (${code}). ${hint}` : base;
142
+ }
35
143
 
36
144
  /**
37
145
  * How a row is stored, or `null` when there is no row — the column is a plain
@@ -78,6 +186,14 @@ interface OAuthBlob {
78
186
  * like the verifier: consumed by the exchange it was minted for.
79
187
  */
80
188
  pendingScopes?: string;
189
+ /**
190
+ * A managed sign-in in progress: the one-time nonce the broker's handoff
191
+ * must echo. Stored encrypted like the verifier, and for the same reason —
192
+ * it binds the handoff to the sign-in this row opened, so a handoff that
193
+ * is replayed or forged lands nowhere. Cleared by the handoff it was
194
+ * minted for.
195
+ */
196
+ pendingNonce?: string;
81
197
  }
82
198
 
83
199
  /**
@@ -94,6 +210,20 @@ interface OAuthBlob {
94
210
  */
95
211
  export class DbSecretsVaultService implements ISecretsVaultService {
96
212
  private cryptoInstance: TokenCrypto | null = null;
213
+ /**
214
+ * How a MANAGED sign-in's tokens are renewed and revoked — through the
215
+ * broker and the provider respectively. Setter-injected: both depend on
216
+ * the connector catalog, which is built after this service. Without them a
217
+ * managed token that expires is served stale (a transient, retried on the
218
+ * next call) and a disconnect deletes without revoking.
219
+ */
220
+ private managedRefresher?: ManagedTokenRefresher;
221
+ private managedRevoker?: ManagedTokenRevoker;
222
+
223
+ setManagedTokenHandlers(handlers: { refresher?: ManagedTokenRefresher; revoker?: ManagedTokenRevoker }): void {
224
+ this.managedRefresher = handlers.refresher;
225
+ this.managedRevoker = handlers.revoker;
226
+ }
97
227
 
98
228
  constructor(
99
229
  private readonly db: Database,
@@ -169,15 +299,20 @@ export class DbSecretsVaultService implements ISecretsVaultService {
169
299
  const label = this.normaliseLabel(input.label);
170
300
  const valueEncrypted = this.crypto().encrypt(input.value);
171
301
 
302
+ // The caller's row may hold a managed sign-in: its tokens are told to the
303
+ // provider before this value replaces them, as on every replacement.
172
304
  // Upsert on (user_id, key): re-saving a key replaces its value in place.
173
- const [row] = await this.db
174
- .insert(secrets)
175
- .values({ userId, key, kind: 'static', label, valueEncrypted, oauthMeta: null })
176
- .onConflictDoUpdate({
177
- target: [secrets.userId, secrets.key],
178
- set: { kind: 'static', label, valueEncrypted, oauthMeta: null, updatedAt: new Date() },
179
- })
180
- .returning();
305
+ const row = await this.revokedReplacement(and(eq(secrets.userId, userId), eq(secrets.key, key)), (guard) =>
306
+ this.db
307
+ .insert(secrets)
308
+ .values({ userId, key, kind: 'static', label, valueEncrypted, oauthMeta: null })
309
+ .onConflictDoUpdate({
310
+ target: [secrets.userId, secrets.key],
311
+ set: { kind: 'static', label, valueEncrypted, oauthMeta: null, updatedAt: new Date() },
312
+ ...(guard ? { setWhere: guard } : {}),
313
+ })
314
+ .returning(),
315
+ );
181
316
  this.notifyMutation(userId);
182
317
  return this.toSummary(row);
183
318
  }
@@ -192,26 +327,28 @@ export class DbSecretsVaultService implements ISecretsVaultService {
192
327
  const valueEncrypted = this.crypto().encrypt(JSON.stringify(blob));
193
328
  const oauthMeta = this.publicProviderMeta(provider);
194
329
 
195
- const [row] = await this.db
196
- .insert(secrets)
197
- .values({ userId, key, kind: 'oauth', label, valueEncrypted, oauthMeta })
198
- .onConflictDoUpdate({
199
- target: [secrets.userId, secrets.key],
200
- // Replacing an oauth secret's provider config drops any existing tokens
201
- // (they belonged to the old client) — the user must re-authorize.
202
- set: { kind: 'oauth', label, valueEncrypted, oauthMeta, updatedAt: new Date() },
203
- })
204
- .returning();
330
+ // A managed sign-in under this key loses its tokens here: the provider is
331
+ // told first, as on every replacement.
332
+ const row = await this.revokedReplacement(and(eq(secrets.userId, userId), eq(secrets.key, key)), (guard) =>
333
+ this.db
334
+ .insert(secrets)
335
+ .values({ userId, key, kind: 'oauth', label, valueEncrypted, oauthMeta })
336
+ .onConflictDoUpdate({
337
+ target: [secrets.userId, secrets.key],
338
+ // Replacing an oauth secret's provider config drops any existing tokens
339
+ // (they belonged to the old client) — the user must re-authorize.
340
+ set: { kind: 'oauth', label, valueEncrypted, oauthMeta, updatedAt: new Date() },
341
+ ...(guard ? { setWhere: guard } : {}),
342
+ })
343
+ .returning(),
344
+ );
205
345
  this.notifyMutation(userId);
206
346
  return this.toSummary(row);
207
347
  }
208
348
 
209
349
  async remove(userId: string, id: string): Promise<void> {
210
- const res = await this.db
211
- .delete(secrets)
212
- .where(and(eq(secrets.id, id), eq(secrets.userId, userId)))
213
- .returning({ id: secrets.id });
214
- if (res.length === 0) throw new SecretNotFoundError(id);
350
+ const gone = await this.revokeThenDelete(and(eq(secrets.id, id), eq(secrets.userId, userId)));
351
+ if (gone.length === 0) throw new SecretNotFoundError(id);
215
352
  this.notifyMutation(userId);
216
353
  }
217
354
 
@@ -226,37 +363,199 @@ export class DbSecretsVaultService implements ISecretsVaultService {
226
363
  const label = this.normaliseLabel(input.label);
227
364
  const valueEncrypted = this.crypto().encrypt(input.value);
228
365
 
366
+ // A shared row that held a managed workspace connection is told to its
367
+ // provider before this value replaces it; the tokens would otherwise go
368
+ // unrevoked with the row's kind.
229
369
  // Upsert the ONE shared row for this key (user_id NULL). The conflict target
230
370
  // is the partial unique index `secrets_shared_key_unq` — hence `targetWhere`.
231
- const [row] = await this.db
232
- .insert(secrets)
233
- .values({ userId: null, key, kind: 'static', label, valueEncrypted, oauthMeta: null })
234
- .onConflictDoUpdate({
235
- target: secrets.key,
236
- targetWhere: isNull(secrets.userId),
237
- set: { kind: 'static', label, valueEncrypted, oauthMeta: null, updatedAt: new Date() },
238
- })
239
- .returning();
371
+ const row = await this.revokedReplacement(and(isNull(secrets.userId), eq(secrets.key, key)), (guard) =>
372
+ this.db
373
+ .insert(secrets)
374
+ .values({ userId: null, key, kind: 'static', label, valueEncrypted, oauthMeta: null })
375
+ .onConflictDoUpdate({
376
+ target: secrets.key,
377
+ targetWhere: isNull(secrets.userId),
378
+ set: { kind: 'static', label, valueEncrypted, oauthMeta: null, updatedAt: new Date() },
379
+ ...(guard ? { setWhere: guard } : {}),
380
+ })
381
+ .returning(),
382
+ );
240
383
  this.notifyMutation(null);
241
384
  return this.toSummary(row);
242
385
  }
243
386
 
244
387
  async removeShared(key: string): Promise<void> {
245
- const res = await this.db
246
- .delete(secrets)
247
- .where(and(isNull(secrets.userId), eq(secrets.key, key)))
248
- .returning({ id: secrets.id });
249
- if (res.length === 0) throw new SecretNotFoundError(key);
388
+ const gone = await this.revokeThenDelete(and(isNull(secrets.userId), eq(secrets.key, key)));
389
+ if (gone.length === 0) throw new SecretNotFoundError(key);
250
390
  this.notifyMutation(null);
251
391
  }
252
392
 
253
393
  async removeUserByKey(userId: string, key: string): Promise<void> {
254
- const res = await this.db
255
- .delete(secrets)
256
- .where(and(eq(secrets.userId, userId), eq(secrets.key, key)))
257
- .returning({ id: secrets.id });
258
- if (res.length === 0) throw new SecretNotFoundError(key);
394
+ const gone = await this.revokeThenDelete(and(eq(secrets.userId, userId), eq(secrets.key, key)));
395
+ if (gone.length === 0) throw new SecretNotFoundError(key);
396
+ this.notifyMutation(userId);
397
+ }
398
+
399
+ /**
400
+ * Delete the rows `where` names, each told to its provider first when it
401
+ * is a managed sign-in with tokens (see {@link revokeRow}). The delete is
402
+ * guarded on the ciphertext the revoke read: a handoff or a refresh landing
403
+ * in between has replaced the tokens, and deleting THOSE unrevoked is the
404
+ * leak the revoke exists to prevent. A miss re-reads the row and goes
405
+ * again, bounded; what could not be settled in those passes is left
406
+ * standing rather than deleted blind.
407
+ */
408
+ private async revokeThenDelete(where: ReturnType<typeof and>): Promise<{ id: string; userId: string | null; kind: string }[]> {
409
+ // No revoker, nothing to read first: one plain delete, as every removal
410
+ // was before managed sign-ins, and no miss to re-read for.
411
+ if (!this.managedRevoker) {
412
+ return this.db.delete(secrets).where(where).returning({ id: secrets.id, userId: secrets.userId, kind: secrets.kind });
413
+ }
414
+ const gone: { id: string; userId: string | null; kind: string }[] = [];
415
+ for (let attempt = 0; attempt < 3; attempt++) {
416
+ const rows = await this.db.select().from(secrets).where(where);
417
+ if (rows.length === 0) break;
418
+ // Every row's revocation at once, so a bulk delete (a namespace, a
419
+ // person's connections) waits one revocation's bound, not one per row.
420
+ await Promise.all(rows.map((row) => this.revokeRow(row)));
421
+ let missed = false;
422
+ for (const row of rows) {
423
+ const deleted = await this.db
424
+ .delete(secrets)
425
+ .where(and(eq(secrets.id, row.id), eq(secrets.valueEncrypted, row.valueEncrypted)))
426
+ .returning({ id: secrets.id, userId: secrets.userId, kind: secrets.kind });
427
+ if (deleted.length === 0) missed = true;
428
+ else gone.push(...deleted);
429
+ }
430
+ if (!missed) break;
431
+ }
432
+ return gone;
433
+ }
434
+
435
+ async removeManagedRowsOfUser(userId: string, within?: ManagedRowsWriter): Promise<number> {
436
+ if (within) return this.removeManagedRowsLocked(userId, within);
437
+ // Every oauth row of the user whose meta names a managed app. Read
438
+ // first (the managed mark lives in JSON the WHERE cannot see), revoked,
439
+ // then deleted by id; the per-user filter on the delete is what keeps a
440
+ // row read for this user from ever deleting another's.
441
+ const rows = await this.db
442
+ .select()
443
+ .from(secrets)
444
+ .where(and(eq(secrets.userId, userId), eq(secrets.kind, 'oauth')));
445
+ const managed = rows.filter((r) => this.managedRefOf(r.oauthMeta) !== null);
446
+ if (managed.length === 0) return 0;
447
+ const deleted = await this.revokeThenDelete(and(eq(secrets.userId, userId), inArray(secrets.id, managed.map((r) => r.id))));
448
+ if (deleted.length > 0) this.notifyMutation(userId);
449
+ // A row the guarded delete could not settle (its tokens kept changing
450
+ // under the revoke) is still here, unrevoked. The caller is told rather
451
+ // than left to believe the person holds nothing: an erasure refuses and
452
+ // is retried, a switch-off logs it and the row is refused on use.
453
+ if (deleted.length < managed.length) {
454
+ throw new SecretOAuthError(
455
+ `${managed.length - deleted.length} managed sign-in(s) kept changing while being revoked and were not removed; try again`,
456
+ );
457
+ }
458
+ return deleted.length;
459
+ }
460
+
461
+ /**
462
+ * The same removal inside a caller's transaction (an account erasure, just
463
+ * before the user row goes): the rows are LOCKED for the transaction, so a
464
+ * handoff or a refresh arriving now waits and then finds no row, which is
465
+ * what makes the revoke final. No guard is needed under the lock; the
466
+ * revoke stays bounded (see {@link revokeRow}), so the transaction never
467
+ * waits on a provider longer than that.
468
+ */
469
+ private async removeManagedRowsLocked(userId: string, tx: ManagedRowsWriter): Promise<number> {
470
+ const rows = await tx
471
+ .select()
472
+ .from(secrets)
473
+ .where(and(eq(secrets.userId, userId), eq(secrets.kind, 'oauth')))
474
+ .for('update');
475
+ const managed = rows.filter((r) => this.managedRefOf(r.oauthMeta) !== null);
476
+ if (managed.length === 0) return 0;
477
+ await Promise.all(managed.map((row) => this.revokeRow(row)));
478
+ await tx.delete(secrets).where(
479
+ and(
480
+ eq(secrets.userId, userId),
481
+ inArray(
482
+ secrets.id,
483
+ managed.map((r) => r.id),
484
+ ),
485
+ ),
486
+ );
259
487
  this.notifyMutation(userId);
488
+ return managed.length;
489
+ }
490
+
491
+ /**
492
+ * Replace what `where` names with `write`, a managed sign-in's tokens told
493
+ * to their provider first (see {@link revokeRow}; best effort, never holding
494
+ * the user's decision). The write is guarded on the ciphertext the revoke
495
+ * read (`setWhere`): a refresh or a handoff landing in between has rotated
496
+ * the tokens, and writing over THOSE unrevoked is the leak the revoke exists
497
+ * to prevent — as the guarded delete of {@link revokeThenDelete}. A miss
498
+ * re-reads and goes again, bounded; a row that kept changing is left as it
499
+ * is and the caller is told. With no revoker wired there is nothing to read
500
+ * first: one plain write, as every replacement was before managed sign-ins.
501
+ */
502
+ private async revokedReplacement<T>(
503
+ where: ReturnType<typeof and>,
504
+ write: (guard?: ReturnType<typeof eq>) => Promise<T[]>,
505
+ ): Promise<T> {
506
+ if (!this.managedRevoker) return (await write())[0] as T;
507
+ for (let attempt = 0; attempt < REPLACE_ATTEMPTS; attempt++) {
508
+ const rows = await this.db.select().from(secrets).where(where);
509
+ await Promise.all(rows.map((row) => this.revokeRow(row)));
510
+ // The row as it was read, or no row at all: a conflict with anything
511
+ // else (a row written meanwhile) is a miss, and the next pass reads it.
512
+ const guard =
513
+ rows.length > 0
514
+ ? inArray(
515
+ secrets.valueEncrypted,
516
+ rows.map((row) => row.valueEncrypted),
517
+ )
518
+ : eq(secrets.valueEncrypted, '');
519
+ const written = await write(guard);
520
+ if (written.length > 0) return written[0] as T;
521
+ }
522
+ throw new SecretOAuthError('the sign-in kept changing while being replaced; try again');
523
+ }
524
+
525
+ private async revokeRow(row: typeof secrets.$inferSelect): Promise<void> {
526
+ if (!this.managedRevoker || row.kind !== 'oauth') return;
527
+ const ref = this.managedRefOf(row.oauthMeta);
528
+ if (!ref) return;
529
+ const tokens = this.readTokensSafe(row.valueEncrypted);
530
+ if (!tokens?.access_token) return;
531
+ // Bounded: the delete that follows is the user's decision, and a revoker
532
+ // that never settles must not hold it.
533
+ try {
534
+ await bounded(
535
+ this.managedRevoker.revoke(ref, { access_token: tokens.access_token, refresh_token: tokens.refresh_token }),
536
+ REVOKE_TIMEOUT_MS,
537
+ 'revocation',
538
+ );
539
+ } catch (err) {
540
+ // The key and the connector name the sign-in. The error itself stays
541
+ // out: a revoker's message is its own, and this line is the one place a
542
+ // token could otherwise reach a log from this path.
543
+ log.warn('revoking a managed sign-in with its provider failed; deleting it here anyway', {
544
+ key: row.key,
545
+ connector: ref.connectorId,
546
+ failure: err instanceof Error ? err.name : typeof err,
547
+ });
548
+ }
549
+ }
550
+
551
+ /** The managed reference a row's public meta carries, or null for an ordinary sign-in. */
552
+ private managedRefOf(raw: unknown): ManagedSignInRef | null {
553
+ const meta = (raw ?? {}) as Record<string, unknown>;
554
+ const m = meta.managed as Record<string, unknown> | undefined;
555
+ if (!m || typeof m !== 'object') return null;
556
+ const connectorId = typeof m.connectorId === 'string' ? m.connectorId : '';
557
+ const scopeSet = typeof m.scopeSet === 'string' ? m.scopeSet : '';
558
+ return connectorId && scopeSet ? { connectorId, scopeSet } : null;
260
559
  }
261
560
 
262
561
  async countNamespace(prefix: string): Promise<NamespaceSecretCount> {
@@ -279,10 +578,10 @@ export class DbSecretsVaultService implements ISecretsVaultService {
279
578
  for (let pass = 0; pass < 5; pass++) {
280
579
  const rows = await this.namespaceRows(prefix);
281
580
  if (rows.length === 0) break;
282
- const deleted = await this.db
283
- .delete(secrets)
284
- .where(inArray(secrets.id, rows.map((r) => r.id)))
285
- .returning({ userId: secrets.userId, kind: secrets.kind });
581
+ // A managed sign-in under the namespace is told to its provider
582
+ // before the delete, for the same reason the per-row removals do —
583
+ // and guarded the same way, when a revoker is there to need it.
584
+ const deleted = await this.revokeThenDelete(inArray(secrets.id, rows.map((r) => r.id)));
286
585
  gone.push(...deleted);
287
586
  }
288
587
  } finally {
@@ -332,6 +631,7 @@ export class DbSecretsVaultService implements ISecretsVaultService {
332
631
  userId: secrets.userId,
333
632
  kind: secrets.kind,
334
633
  valueEncrypted: secrets.valueEncrypted,
634
+ oauthMeta: secrets.oauthMeta,
335
635
  })
336
636
  .from(secrets)
337
637
  .where(and(inArray(secrets.key, keys), or(isNull(secrets.userId), eq(secrets.userId, userId))));
@@ -340,6 +640,15 @@ export class DbSecretsVaultService implements ISecretsVaultService {
340
640
  const adminRow = rows.find((r) => r.key === key && r.userId === null);
341
641
  // Decrypt+parse the oauth row's token set once, then derive both fields from it.
342
642
  const tokens = userRow?.kind === 'oauth' ? this.readTokensSafe(userRow.valueEncrypted) : undefined;
643
+ // The shared row holds tokens only for a managed workspace connection;
644
+ // an ordinary sign-in's shared row is the client secret and reads as
645
+ // "no tokens" here, which is the truthful answer for it.
646
+ const adminTokens = adminRow?.kind === 'oauth' ? this.readTokensSafe(adminRow.valueEncrypted) : undefined;
647
+ const adminMeta = (adminRow?.oauthMeta ?? null) as Record<string, unknown> | null;
648
+ const accountOf = (meta: unknown): string | undefined => {
649
+ const a = (meta as Record<string, unknown> | null)?.account;
650
+ return typeof a === 'string' && a ? a : undefined;
651
+ };
343
652
  return {
344
653
  key,
345
654
  adminConfigured: !!adminRow,
@@ -353,10 +662,157 @@ export class DbSecretsVaultService implements ISecretsVaultService {
353
662
  // Only meaningful for an oauth row: the scopes the caller's token was granted,
354
663
  // so a coverage check can tell whether it still covers the tool's live scopes.
355
664
  grantedScopes: userRow?.kind === 'oauth' ? tokens?.scope : undefined,
665
+ adminAuthorized: adminRow?.kind === 'oauth' ? Boolean(adminTokens?.access_token) : undefined,
666
+ adminGrantedScopes: adminRow?.kind === 'oauth' ? adminTokens?.scope : undefined,
667
+ userAccount: userRow?.kind === 'oauth' ? accountOf(userRow.oauthMeta) : undefined,
668
+ adminAccount: adminRow?.kind === 'oauth' ? accountOf(adminMeta) : undefined,
669
+ // An owner's own app on the shared row: a client id without the managed mark.
670
+ adminBringsOwnApp:
671
+ adminRow?.kind === 'oauth'
672
+ ? this.managedRefOf(adminMeta) === null && typeof adminMeta?.clientId === 'string' && adminMeta.clientId !== ''
673
+ : undefined,
356
674
  };
357
675
  });
358
676
  }
359
677
 
678
+ async beginManagedConnection(input: {
679
+ userId: string | null;
680
+ key: string;
681
+ provider: OAuthProviderConfig;
682
+ nonce: string;
683
+ }): Promise<void> {
684
+ const key = this.requireKey(input.key);
685
+ const userId = input.userId === null ? null : this.requireUserId(input.userId);
686
+ const ref = input.provider.managed;
687
+ if (!ref) throw new InvalidSecretError('a managed sign-in needs its connector reference');
688
+ if (typeof input.nonce !== 'string' || input.nonce.length < 16) throw new InvalidSecretError('nonce is required');
689
+ const oauthMeta = this.publicProviderMeta(this.requireProvider(input.provider, { managed: true }));
690
+ const where = userId === null ? and(isNull(secrets.userId), eq(secrets.key, key)) : and(eq(secrets.userId, userId), eq(secrets.key, key));
691
+ // Read-modify-write on a LIVE credential: a refresh running concurrently
692
+ // may rotate the tokens between the read and the write, and writing our
693
+ // copy over them would leave the row holding a dead refresh token. So the
694
+ // update is guarded on the ciphertext read, and a miss re-reads and
695
+ // rebuilds from the fresh row; bounded, so a row that keeps changing
696
+ // fails loudly rather than forever. The same rule `beginOAuth` follows.
697
+ for (let attempt = 0; attempt < BEGIN_MANAGED_ATTEMPTS; attempt++) {
698
+ const [existing] = await this.db.select().from(secrets).where(where).limit(1);
699
+ // Keep what the SAME connection already holds (its tokens, so an
700
+ // abandoned reconnect costs nothing). The same connection means the same
701
+ // connector and scope set: a row of another connector's, or of another
702
+ // set's, holds a token for something else, and a declaration that
703
+ // changed must not hand it to the new one. Start clean over anything
704
+ // else too — a static value or an owner-app sign-in under this key.
705
+ const storedRef = existing?.kind === 'oauth' ? this.managedRefOf(existing.oauthMeta) : null;
706
+ const sameConnection =
707
+ storedRef !== null && storedRef.connectorId === ref.connectorId && storedRef.scopeSet === ref.scopeSet;
708
+ let kept: OAuthBlob = {};
709
+ if (existing && sameConnection) {
710
+ try {
711
+ kept = this.readBlob(existing.valueEncrypted);
712
+ } catch {
713
+ kept = {};
714
+ }
715
+ }
716
+ const next: OAuthBlob = { tokens: kept.tokens, pendingNonce: input.nonce };
717
+ const valueEncrypted = this.crypto().encrypt(JSON.stringify(next));
718
+ const account = sameConnection ? (existing!.oauthMeta as Record<string, unknown> | null)?.account : undefined;
719
+ const meta = typeof account === 'string' && account ? { ...oauthMeta, account } : oauthMeta;
720
+ if (!existing) {
721
+ try {
722
+ await this.db.insert(secrets).values({ userId, key, kind: 'oauth', label: null, valueEncrypted, oauthMeta: meta });
723
+ return;
724
+ } catch (err) {
725
+ // Another start of the same sign-in inserted the row first: the
726
+ // next pass finds it and updates it, as it would any existing row.
727
+ if (pgErrorCode(err) === '23505') continue; // unique_violation
728
+ throw err;
729
+ }
730
+ }
731
+ // A row that held ANOTHER connection's tokens is about to lose them:
732
+ // the provider is told first, as on every other replacement.
733
+ if (!sameConnection && storedRef) await this.revokeRow(existing);
734
+ const written = await this.db
735
+ .update(secrets)
736
+ .set({ kind: 'oauth', valueEncrypted, oauthMeta: meta, updatedAt: new Date() })
737
+ .where(and(eq(secrets.id, existing.id), eq(secrets.valueEncrypted, existing.valueEncrypted)))
738
+ .returning({ id: secrets.id });
739
+ if (Array.isArray(written) && written.length === 0) continue;
740
+ // A row that held something ELSE (a value, another connection's
741
+ // tokens, an owner's app) no longer does: caches keyed on it are told.
742
+ // The same connection's tokens carried over unchanged tell nobody.
743
+ if (!kept.tokens) this.notifyMutation(userId);
744
+ return;
745
+ }
746
+ throw new SecretOAuthError('The sign-in could not be opened: its stored credential kept changing. Try again.');
747
+ }
748
+
749
+ async completeManagedConnection(input: {
750
+ userId: string | null;
751
+ key: string;
752
+ nonce: string;
753
+ tokens: DeliveredTokenSet;
754
+ account?: string;
755
+ }): Promise<void> {
756
+ const key = this.requireKey(input.key);
757
+ const userId = input.userId === null ? null : this.requireUserId(input.userId);
758
+ const where = userId === null ? and(isNull(secrets.userId), eq(secrets.key, key)) : and(eq(secrets.userId, userId), eq(secrets.key, key));
759
+ // Every refusal below is the same words: a caller that can tell "no such
760
+ // sign-in" from "wrong nonce" from "already used" has learned which rows
761
+ // exist, and the handoff route is reachable with the broker secret alone.
762
+ const refused = () => new SecretOAuthError('No managed sign-in is waiting for this handoff');
763
+ const tokens = input.tokens;
764
+ if (typeof tokens?.access_token !== 'string' || !tokens.access_token) {
765
+ throw new InvalidSecretError('the handoff carries no access token');
766
+ }
767
+ // Read, check the nonce, write guarded on what was read; and again when
768
+ // the write misses. A miss with the nonce still on the row is a refresh
769
+ // that rotated the row's tokens meanwhile (a refresh carries the nonce
770
+ // across, see `inFlightOf`), not a second handoff: the broker delivered
771
+ // these tokens once, and refusing them would lose a valid sign-in for
772
+ // good. A row whose nonce is gone or changed refuses on the re-read.
773
+ for (let attempt = 0; ; attempt++) {
774
+ const [row] = await this.db.select().from(secrets).where(where).limit(1);
775
+ if (!row || row.kind !== 'oauth' || !this.managedRefOf(row.oauthMeta)) throw refused();
776
+ let blob: OAuthBlob;
777
+ try {
778
+ blob = this.readBlob(row.valueEncrypted);
779
+ } catch {
780
+ throw refused();
781
+ }
782
+ if (!blob.pendingNonce || typeof input.nonce !== 'string' || !timingSafeStringEqual(input.nonce, blob.pendingNonce)) {
783
+ throw refused();
784
+ }
785
+ const meta = { ...((row.oauthMeta ?? {}) as Record<string, unknown>) };
786
+ // A broker that echoes no `scope` delivered what the sign-in asked for:
787
+ // the scopes the row was opened with stand in, as `pendingScopes` does
788
+ // for the code exchange. Stored as unknown, the grant would count as
789
+ // covering nothing, and the sign-in would read as under-scoped for good.
790
+ // An EMPTY scope is not "none echoed": it is an empty grant, kept as such.
791
+ const requested = Array.isArray(meta.scopes) && meta.scopes.length > 0 ? meta.scopes.map(String).join(' ') : undefined;
792
+ const stored: OAuthTokenSet = {
793
+ access_token: tokens.access_token,
794
+ refresh_token: typeof tokens.refresh_token === 'string' ? tokens.refresh_token : undefined,
795
+ expires_at: typeof tokens.expires_at === 'number' ? tokens.expires_at : undefined,
796
+ token_type: typeof tokens.token_type === 'string' ? tokens.token_type : undefined,
797
+ scope: typeof tokens.scope === 'string' ? tokens.scope : requested,
798
+ };
799
+ // The nonce is one-time: it leaves with the handoff it was minted for,
800
+ // so the same handoff presented twice finds nothing to match.
801
+ const next: OAuthBlob = { tokens: stored };
802
+ if (typeof input.account === 'string' && input.account.trim()) meta.account = input.account.trim().slice(0, MAX_LABEL_LEN);
803
+ // Guarded on the ciphertext read above: two handoffs racing for one
804
+ // nonce cannot both land, whichever the provider sent first wins.
805
+ const landed = await this.db
806
+ .update(secrets)
807
+ .set({ valueEncrypted: this.crypto().encrypt(JSON.stringify(next)), oauthMeta: meta, updatedAt: new Date() })
808
+ .where(and(eq(secrets.id, row.id), eq(secrets.valueEncrypted, row.valueEncrypted)))
809
+ .returning({ id: secrets.id });
810
+ if (!(Array.isArray(landed) && landed.length === 0)) break;
811
+ if (attempt + 1 >= HANDOFF_ATTEMPTS) throw refused();
812
+ }
813
+ this.notifyMutation(userId);
814
+ }
815
+
360
816
  /**
361
817
  * Decrypt+parse an oauth row's token set once, or undefined if the blob can't be
362
818
  * read — a corrupted row or rotated encKey must degrade to "not authorized", not throw.
@@ -487,20 +943,27 @@ export class DbSecretsVaultService implements ISecretsVaultService {
487
943
  throw new InvalidSecretError('clientSecret is required');
488
944
  }
489
945
  // Validate the provider config the same way createOAuth does (SSRF-safe https
490
- // URLs). Store the client secret AND the provider meta together on the shared
491
- // row so a later `.tool` edit can't redirect the secret to another endpoint.
946
+ // URLs) and store it beside the secret as a record of what was saved — what
947
+ // `statusFor` and the setup surfaces read. A declared sign-in does not run
948
+ // from it: `beginToolOAuthByKey` is handed the declaration as it reads now.
492
949
  const provider = this.requireProvider({ ...input.provider, clientSecret: input.clientSecret });
493
950
  const blob: OAuthBlob = { clientSecret: provider.clientSecret };
494
951
  const valueEncrypted = this.crypto().encrypt(JSON.stringify(blob));
495
952
  const oauthMeta = this.publicProviderMeta(provider);
496
- await this.db
497
- .insert(secrets)
498
- .values({ userId: null, key, kind: 'oauth', label: null, valueEncrypted, oauthMeta })
499
- .onConflictDoUpdate({
500
- target: secrets.key,
501
- targetWhere: isNull(secrets.userId),
502
- set: { kind: 'oauth', valueEncrypted, oauthMeta, updatedAt: new Date() },
503
- });
953
+ // An owner's own app replacing a managed workspace connection: the
954
+ // connection's tokens are revoked with the provider before they go.
955
+ await this.revokedReplacement(and(isNull(secrets.userId), eq(secrets.key, key)), (guard) =>
956
+ this.db
957
+ .insert(secrets)
958
+ .values({ userId: null, key, kind: 'oauth', label: null, valueEncrypted, oauthMeta })
959
+ .onConflictDoUpdate({
960
+ target: secrets.key,
961
+ targetWhere: isNull(secrets.userId),
962
+ set: { kind: 'oauth', valueEncrypted, oauthMeta, updatedAt: new Date() },
963
+ ...(guard ? { setWhere: guard } : {}),
964
+ })
965
+ .returning({ id: secrets.id }),
966
+ );
504
967
  this.notifyMutation(null);
505
968
  }
506
969
 
@@ -510,11 +973,15 @@ export class DbSecretsVaultService implements ISecretsVaultService {
510
973
  redirectUri: string;
511
974
  state: string;
512
975
  scopes?: string[];
976
+ provider?: DeclaredOAuthProvider;
513
977
  }): Promise<{ id: string; url: string }> {
514
978
  const userId = this.requireUserId(input.userId);
515
979
  const key = this.requireKey(input.key);
516
- // The shared row holds the owner-set client secret + the provider meta it was
517
- // set for. Both are read from HERE, never from the live `.tool`.
980
+ // The shared row holds the owner-set client secret. For a client the
981
+ // platform registered itself it also holds the provider configuration, the
982
+ // only description of that client there is; for a declared sign-in the
983
+ // configuration it holds is a record of the save, and `input.provider` (the
984
+ // tool file as it reads now) is what the sign-in follows.
518
985
  const [shared] = await this.db
519
986
  .select()
520
987
  .from(secrets)
@@ -524,11 +991,46 @@ export class DbSecretsVaultService implements ISecretsVaultService {
524
991
  throw new SecretOAuthError("This tool's owner hasn't finished setting this up");
525
992
  }
526
993
  const sharedBlob = this.readBlob(shared.valueEncrypted);
527
- const meta = this.readMeta(shared.oauthMeta);
528
- // A missing secret is "not set up yet" ONLY for confidential clients — a
529
- // dynamically-registered PUBLIC client (PKCE-only) never has one.
530
- if (!sharedBlob.clientSecret && !meta.publicClient) {
531
- throw new SecretOAuthError("This tool's owner hasn't finished setting this up");
994
+ let meta: OAuthProviderConfig;
995
+ let oauthMeta: typeof shared.oauthMeta;
996
+ if (input.provider) {
997
+ // A declared client is confidential: without the owner's secret there is
998
+ // nothing to sign in with, whatever the stored row says of itself.
999
+ if (!sharedBlob.clientSecret) {
1000
+ throw new SecretOAuthError("This tool's owner hasn't finished setting this up");
1001
+ }
1002
+ // The save's own validation (https, no private or local address), BEFORE
1003
+ // the caller's row is written or a URL built: a declaration that fails it
1004
+ // sends nothing anywhere. A MANAGED declaration names no client of its
1005
+ // own; reaching here means an owner pinned the deployment's own app on
1006
+ // the shared row, and that app's id is the one the flow runs with.
1007
+ const clientId = input.provider.clientId || this.readMeta(shared.oauthMeta).clientId;
1008
+ meta = this.requireProvider({ ...input.provider, clientId, publicClient: false });
1009
+ oauthMeta = this.publicProviderMeta(meta);
1010
+ // A MOVED PROPERTY, decided on the ticket (OAuth-Sign-In-Follows-The-Live-
1011
+ // Declaration, 2026-10-27): the secret used to be bound to the token
1012
+ // endpoint pinned at its save, so a later file edit could not send it
1013
+ // anywhere else; now it goes where the file says at each start, so a
1014
+ // writer of the tool file can move it (to an https, public host — the
1015
+ // validation above still holds). Nothing refuses that; this line is the
1016
+ // trail an operator reads when a secret went somewhere new.
1017
+ const recorded = (shared.oauthMeta ?? {}) as { tokenUrl?: unknown };
1018
+ if (typeof recorded.tokenUrl === 'string' && recorded.tokenUrl !== meta.tokenUrl) {
1019
+ // Origins only: a path or query is the file's to say, not the log's.
1020
+ const origin = (u: string) => (URL.canParse(u) ? new URL(u).origin : '(unparseable)');
1021
+ log.warn(`sign-in on ${key}: the declared token endpoint differs from the one recorded when the client secret was saved`, {
1022
+ savedOrigin: origin(recorded.tokenUrl),
1023
+ declaredOrigin: origin(meta.tokenUrl),
1024
+ });
1025
+ }
1026
+ } else {
1027
+ meta = this.readMeta(shared.oauthMeta);
1028
+ oauthMeta = shared.oauthMeta;
1029
+ // A missing secret is "not set up yet" ONLY for confidential clients — a
1030
+ // dynamically-registered PUBLIC client (PKCE-only) never has one.
1031
+ if (!sharedBlob.clientSecret && !meta.publicClient) {
1032
+ throw new SecretOAuthError("This tool's owner hasn't finished setting this up");
1033
+ }
532
1034
  }
533
1035
 
534
1036
  // Carry forward any tokens the caller already holds so re-entering the flow
@@ -543,12 +1045,12 @@ export class DbSecretsVaultService implements ISecretsVaultService {
543
1045
  // encKey must degrade to "start fresh", not crash the recovery flow — and (b)
544
1046
  // they were minted under the SAME provider config we're about to (re)write, or
545
1047
  // a refresh_token for a stale client id/secret would silently fail once carried
546
- // into this row after an owner rotates the shared secret.
1048
+ // into this row after an owner rotates the shared secret or edits the
1049
+ // declaration. Compared as values, not as serialised text: the stored side
1050
+ // has been through jsonb, which does not keep key order, and a declared
1051
+ // sign-in's side is built here.
547
1052
  let existingTokens: OAuthTokenSet | undefined;
548
- if (
549
- existingUserRow?.kind === 'oauth' &&
550
- JSON.stringify(existingUserRow.oauthMeta) === JSON.stringify(shared.oauthMeta)
551
- ) {
1053
+ if (existingUserRow?.kind === 'oauth' && isDeepStrictEqual(existingUserRow.oauthMeta, oauthMeta)) {
552
1054
  try {
553
1055
  existingTokens = this.readBlob(existingUserRow.valueEncrypted).tokens;
554
1056
  } catch {
@@ -567,9 +1069,13 @@ export class DbSecretsVaultService implements ISecretsVaultService {
567
1069
  const requestedScopes = input.scopes && input.scopes.length ? input.scopes : meta.scopes;
568
1070
  const pendingScopes = requestedScopes && requestedScopes.length ? requestedScopes.join(' ') : undefined;
569
1071
 
570
- // Provision (or reset) the caller's own oauth row for this key from the shared
571
- // provider meta + secret, preserving any existing tokens. Keyed `<manual>_<VAR>`
572
- // so `resolve` (scope 'user') returns the token once sign-in completes.
1072
+ // Provision (or reset) the caller's own oauth row for this key from the provider
1073
+ // configuration this start uses + the shared secret, preserving any existing
1074
+ // tokens. The row records that configuration because `completeOAuth` and every
1075
+ // later refresh of this grant read it from HERE: the exchange goes to the token
1076
+ // endpoint, under the client id, that the consent was given for. Keyed
1077
+ // `<manual>_<VAR>` so `resolve` (scope 'user') returns the token once sign-in
1078
+ // completes.
573
1079
  const blob: OAuthBlob = {
574
1080
  clientSecret: sharedBlob.clientSecret,
575
1081
  tokens: existingTokens,
@@ -577,18 +1083,25 @@ export class DbSecretsVaultService implements ISecretsVaultService {
577
1083
  pendingScopes,
578
1084
  };
579
1085
  const valueEncrypted = this.crypto().encrypt(JSON.stringify(blob));
580
- const [row] = await this.db
581
- .insert(secrets)
582
- .values({ userId, key, kind: 'oauth', label: shared.label, valueEncrypted, oauthMeta: shared.oauthMeta })
583
- .onConflictDoUpdate({
584
- target: [secrets.userId, secrets.key],
585
- set: { kind: 'oauth', valueEncrypted, oauthMeta: shared.oauthMeta, updatedAt: new Date() },
586
- })
587
- .returning();
588
-
589
- // Build the consent URL exactly as beginOAuth does, from the stored meta —
590
- // EXCEPT the requested scopes (above). Client id, addresses, and secret
591
- // stay owner-pinned.
1086
+ // A managed sign-in on the caller's row (an owner pinned their own app
1087
+ // after the person connected through the broker) is replaced by this
1088
+ // start: its tokens are told to the provider first, and the write is
1089
+ // guarded on what the revoke read. Tokens carried over above were minted
1090
+ // under THIS configuration, never a managed one.
1091
+ const row = await this.revokedReplacement(and(eq(secrets.userId, userId), eq(secrets.key, key)), (guard) =>
1092
+ this.db
1093
+ .insert(secrets)
1094
+ .values({ userId, key, kind: 'oauth', label: shared.label, valueEncrypted, oauthMeta })
1095
+ .onConflictDoUpdate({
1096
+ target: [secrets.userId, secrets.key],
1097
+ set: { kind: 'oauth', valueEncrypted, oauthMeta, updatedAt: new Date() },
1098
+ ...(guard ? { setWhere: guard } : {}),
1099
+ })
1100
+ .returning(),
1101
+ );
1102
+
1103
+ // Build the consent URL exactly as beginOAuth does, from the configuration
1104
+ // this start uses, with the requested scopes chosen above.
592
1105
  const url = new URL(meta.authorizationUrl);
593
1106
  url.searchParams.set('response_type', 'code');
594
1107
  url.searchParams.set('client_id', meta.clientId);
@@ -660,10 +1173,16 @@ export class DbSecretsVaultService implements ISecretsVaultService {
660
1173
  }
661
1174
 
662
1175
  async forceRefresh(userId: string, key: string): Promise<ForcedRefreshOutcome> {
1176
+ // The caller's own row, or — for a variable the manual declares shared
1177
+ // (a managed workspace connection) — the one shared row every caller
1178
+ // resolves. Same tier rule as `resolve`, so a 401 against a shared
1179
+ // token repairs the row the call actually used.
1180
+ const scope = (await this.scopeOf?.(key)) ?? 'admin';
1181
+ const scopeFilter = scope === 'user' ? eq(secrets.userId, userId) : isNull(secrets.userId);
663
1182
  const [row] = await this.db
664
1183
  .select()
665
1184
  .from(secrets)
666
- .where(and(eq(secrets.userId, userId), eq(secrets.key, key)))
1185
+ .where(and(scopeFilter, eq(secrets.key, key)))
667
1186
  .limit(1);
668
1187
  if (!row || row.kind !== 'oauth') return 'skipped';
669
1188
  let blob: OAuthBlob;
@@ -710,17 +1229,38 @@ export class DbSecretsVaultService implements ISecretsVaultService {
710
1229
  return this.wipeTokens(row, blob);
711
1230
  }
712
1231
  const meta = this.readMeta(row.oauthMeta);
713
- const body = new URLSearchParams({
714
- grant_type: 'refresh_token',
715
- refresh_token: tokens.refresh_token,
716
- client_id: meta.clientId,
717
- });
718
- if (blob.clientSecret) body.set('client_secret', blob.clientSecret);
719
- if (meta.resource) body.set('resource', meta.resource);
720
-
721
1232
  let refreshed: OAuthTokenSet;
722
1233
  try {
723
- refreshed = await this.tokenRequest(meta.tokenUrl, body);
1234
+ if (meta.managed) {
1235
+ // A managed sign-in renews through the broker, which holds the
1236
+ // client secret this deployment never sees. No broker wired is an
1237
+ // outage from here, not a dead grant: the stale token is served and
1238
+ // the next call asks again.
1239
+ if (!this.managedRefresher) return { outcome: 'transient' };
1240
+ // Bounded like a revocation: a broker that never answers is an outage
1241
+ // (the stale token is served) and must not hold every resolution.
1242
+ const delivered = await bounded(
1243
+ this.managedRefresher.refresh(meta.managed, tokens.refresh_token),
1244
+ REFRESH_TIMEOUT_MS,
1245
+ 'the broker refresh',
1246
+ );
1247
+ refreshed = {
1248
+ access_token: delivered.access_token,
1249
+ refresh_token: delivered.refresh_token,
1250
+ expires_at: delivered.expires_at,
1251
+ token_type: delivered.token_type,
1252
+ scope: delivered.scope,
1253
+ };
1254
+ } else {
1255
+ const body = new URLSearchParams({
1256
+ grant_type: 'refresh_token',
1257
+ refresh_token: tokens.refresh_token,
1258
+ client_id: meta.clientId,
1259
+ });
1260
+ if (blob.clientSecret) body.set('client_secret', blob.clientSecret);
1261
+ if (meta.resource) body.set('resource', meta.resource);
1262
+ refreshed = await this.tokenRequest(meta.tokenUrl, body);
1263
+ }
724
1264
  } catch (err) {
725
1265
  // A DEFINITIVE provider rejection (400/401 — invalid_grant and friends)
726
1266
  // means the grant is dead: no future refresh will revive it. Wipe the
@@ -732,7 +1272,23 @@ export class DbSecretsVaultService implements ISecretsVaultService {
732
1272
  err instanceof SecretOAuthError &&
733
1273
  (err.providerStatus === 400 || err.providerStatus === 401)
734
1274
  ) {
735
- return this.wipeTokens(row, blob);
1275
+ // The only trace of a sign-in that silently turns into "not connected":
1276
+ // say which one, the provider's reason, and what the guarded wipe did —
1277
+ // a concurrent refresh may have rotated the tokens first, in which case
1278
+ // nothing was wiped and the rotated grant is served.
1279
+ const result = await this.wipeTokens(row, blob);
1280
+ const did = result.wiped
1281
+ ? 'sign-in wiped'
1282
+ : result.outcome === 'refreshed'
1283
+ ? 'the row had been rotated by a concurrent refresh meanwhile; the rotated sign-in is kept'
1284
+ : 'the row had been removed or replaced meanwhile; nothing wiped';
1285
+ // A managed row's refusal came from the refresher, whose words are its
1286
+ // own (a port may put the refresh token it was handed in them): only
1287
+ // the status it reported is logged. The vault's own token request
1288
+ // writes fixed sentences, which are safe to repeat.
1289
+ const why = meta.managed ? `the broker refused it (HTTP ${err.providerStatus ?? 'unknown'})` : err.message;
1290
+ log.warn(`refresh refused for "${row.key}", ${did}: ${why}`);
1291
+ return result;
736
1292
  }
737
1293
  return { outcome: 'transient' };
738
1294
  }
@@ -743,7 +1299,10 @@ export class DbSecretsVaultService implements ISecretsVaultService {
743
1299
  // rule as the exchange: only an ABSENT field means "unchanged"; an echoed
744
1300
  // value, empty included, is the provider's word.
745
1301
  if (refreshed.scope === undefined) refreshed.scope = tokens.scope;
746
- const next: OAuthBlob = { clientSecret: blob.clientSecret, tokens: refreshed };
1302
+ // A sign-in in flight on this row (a managed reconnect waiting for its
1303
+ // handoff, a consent waiting for its exchange) survives the refresh: the
1304
+ // nonce or verifier it minted is what that handoff must match.
1305
+ const next: OAuthBlob = { clientSecret: blob.clientSecret, tokens: refreshed, ...this.inFlightOf(blob) };
747
1306
 
748
1307
  // Optimistic concurrency: only persist if the stored ciphertext is unchanged,
749
1308
  // so a concurrent refresh on the same row doesn't clobber (Microsoft pattern).
@@ -790,12 +1349,30 @@ export class DbSecretsVaultService implements ISecretsVaultService {
790
1349
  }
791
1350
  }
792
1351
 
793
- /** Drop a dead grant's token set, keeping the client secret. See {@link refreshRow}. */
1352
+ /**
1353
+ * What a row holds for a sign-in IN FLIGHT: the one-time nonce a managed
1354
+ * handoff must echo, the PKCE verifier and requested scopes a code exchange
1355
+ * must present. A refresh or a wipe rewrites the row's tokens, never these:
1356
+ * dropping them would refuse the handoff or the exchange that is about to
1357
+ * land, for a reason the person could not see.
1358
+ */
1359
+ private inFlightOf(blob: OAuthBlob): Pick<OAuthBlob, 'pendingNonce' | 'pendingVerifier' | 'pendingScopes'> {
1360
+ return {
1361
+ ...(blob.pendingNonce ? { pendingNonce: blob.pendingNonce } : {}),
1362
+ ...(blob.pendingVerifier ? { pendingVerifier: blob.pendingVerifier } : {}),
1363
+ ...(blob.pendingScopes ? { pendingScopes: blob.pendingScopes } : {}),
1364
+ };
1365
+ }
1366
+
1367
+ /** Drop a dead grant's token set, keeping the client secret and any sign-in in flight. See {@link refreshRow}. */
794
1368
  private async wipeTokens(
795
1369
  row: typeof secrets.$inferSelect,
796
1370
  blob: OAuthBlob,
797
- ): Promise<{ outcome: 'refreshed'; accessToken: string } | { outcome: 'rejected'; accessToken: null }> {
798
- const next: OAuthBlob = { clientSecret: blob.clientSecret };
1371
+ ): Promise<
1372
+ | { outcome: 'refreshed'; accessToken: string; wiped: false }
1373
+ | { outcome: 'rejected'; accessToken: null; wiped: boolean }
1374
+ > {
1375
+ const next: OAuthBlob = { clientSecret: blob.clientSecret, ...this.inFlightOf(blob) };
799
1376
  // Guarded on the ciphertext we read, so a concurrent refresh that
800
1377
  // already persisted ROTATED tokens can't be wiped by our stale
801
1378
  // failure. When that guard trips (0 rows), the failure was against a
@@ -807,13 +1384,17 @@ export class DbSecretsVaultService implements ISecretsVaultService {
807
1384
  .where(and(eq(secrets.id, row.id), eq(secrets.valueEncrypted, row.valueEncrypted)))
808
1385
  .returning({ id: secrets.id });
809
1386
  if (Array.isArray(wiped) && wiped.length === 0) {
1387
+ // `wiped: false` either way: this write matched nothing, and the caller
1388
+ // reports what it did, not what it set out to do.
810
1389
  const fresh = await this.currentAccessToken(row.id);
811
- return fresh ? { outcome: 'refreshed', accessToken: fresh } : { outcome: 'rejected', accessToken: null };
1390
+ return fresh
1391
+ ? { outcome: 'refreshed', accessToken: fresh, wiped: false }
1392
+ : { outcome: 'rejected', accessToken: null, wiped: false };
812
1393
  }
813
1394
  // The sign-in just became not-connected — let credential-validity caches
814
1395
  // (pooled downstream connections dialed with the dead token) drop it now.
815
1396
  this.notifyMutation(row.userId);
816
- return { outcome: 'rejected', accessToken: null };
1397
+ return { outcome: 'rejected', accessToken: null, wiped: true };
817
1398
  }
818
1399
 
819
1400
  async putSharedOAuthProvider(input: {
@@ -829,17 +1410,21 @@ export class DbSecretsVaultService implements ISecretsVaultService {
829
1410
  const blob: OAuthBlob = { clientSecret: provider.clientSecret };
830
1411
  const valueEncrypted = this.crypto().encrypt(JSON.stringify(blob));
831
1412
  const oauthMeta = this.publicProviderMeta(provider);
832
- await this.db
833
- .insert(secrets)
834
- .values({ userId: null, key, kind: 'oauth', label, valueEncrypted, oauthMeta })
835
- // Preserve an existing row's label on re-upsert (matches
836
- // `putSharedOAuthClientSecret`): the optional input label is only for the
837
- // FIRST write, so a later re-registration can't null out an owner label.
838
- .onConflictDoUpdate({
839
- target: secrets.key,
840
- targetWhere: isNull(secrets.userId),
841
- set: { kind: 'oauth', valueEncrypted, oauthMeta, updatedAt: new Date() },
842
- });
1413
+ await this.revokedReplacement(and(isNull(secrets.userId), eq(secrets.key, key)), (guard) =>
1414
+ this.db
1415
+ .insert(secrets)
1416
+ .values({ userId: null, key, kind: 'oauth', label, valueEncrypted, oauthMeta })
1417
+ // Preserve an existing row's label on re-upsert (matches
1418
+ // `putSharedOAuthClientSecret`): the optional input label is only for the
1419
+ // FIRST write, so a later re-registration can't null out an owner label.
1420
+ .onConflictDoUpdate({
1421
+ target: secrets.key,
1422
+ targetWhere: isNull(secrets.userId),
1423
+ set: { kind: 'oauth', valueEncrypted, oauthMeta, updatedAt: new Date() },
1424
+ ...(guard ? { setWhere: guard } : {}),
1425
+ })
1426
+ .returning({ id: secrets.id }),
1427
+ );
843
1428
  this.notifyMutation(null);
844
1429
  }
845
1430
 
@@ -887,7 +1472,15 @@ export class DbSecretsVaultService implements ISecretsVaultService {
887
1472
  throw new SecretOAuthError(msg);
888
1473
  }
889
1474
  if (!res.ok) {
890
- throw new SecretOAuthError(`Token endpoint returned HTTP ${res.status}`, res.status);
1475
+ // The provider's error CODE rides in the message: the callback route and
1476
+ // the refresh path log that message, and it is the one line that says
1477
+ // WHY a sign-in failed (a client the provider does not accept, a refused
1478
+ // authorization code). Best effort: the STATUS is the contract (the
1479
+ // refresh path reads 400/401 off it to retire a dead grant), and a body
1480
+ // that fails to read must not turn a definitive refusal into an
1481
+ // unclassified error.
1482
+ const code = tokenEndpointErrorCode(await readCapped(res, TOKEN_ERROR_BODY_MAX_BYTES).catch(() => ''));
1483
+ throw new SecretOAuthError(describeTokenEndpointRefusal(res.status, code), res.status);
891
1484
  }
892
1485
  const json = (await res.json()) as Record<string, unknown>;
893
1486
  const accessToken = typeof json.access_token === 'string' ? json.access_token : '';
@@ -923,18 +1516,22 @@ export class DbSecretsVaultService implements ISecretsVaultService {
923
1516
  }
924
1517
 
925
1518
  private readMeta(raw: unknown): Required<Pick<OAuthProviderConfig, 'authorizationUrl' | 'tokenUrl' | 'clientId'>> &
926
- Pick<OAuthProviderConfig, 'scopes' | 'authParams' | 'pkce' | 'publicClient' | 'resource'> {
1519
+ Pick<OAuthProviderConfig, 'scopes' | 'authParams' | 'pkce' | 'publicClient' | 'resource' | 'managed'> {
927
1520
  const meta = (raw ?? {}) as Record<string, unknown>;
928
1521
  const authorizationUrl = typeof meta.authorizationUrl === 'string' ? meta.authorizationUrl : '';
929
1522
  const tokenUrl = typeof meta.tokenUrl === 'string' ? meta.tokenUrl : '';
930
1523
  const clientId = typeof meta.clientId === 'string' ? meta.clientId : '';
931
- if (!authorizationUrl || !tokenUrl || !clientId) {
1524
+ const managed = this.managedRefOf(meta) ?? undefined;
1525
+ // A managed row has no client id of its own (the broker's app is the
1526
+ // client); everything else must name one, or no flow can run from it.
1527
+ if (!authorizationUrl || !tokenUrl || (!clientId && !managed)) {
932
1528
  throw new SecretOAuthError('OAuth secret is missing provider configuration');
933
1529
  }
934
1530
  return {
935
1531
  authorizationUrl,
936
1532
  tokenUrl,
937
1533
  clientId,
1534
+ managed,
938
1535
  scopes: Array.isArray(meta.scopes) ? meta.scopes.map(String) : undefined,
939
1536
  authParams:
940
1537
  meta.authParams && typeof meta.authParams === 'object'
@@ -956,16 +1553,27 @@ export class DbSecretsVaultService implements ISecretsVaultService {
956
1553
  ...(p.pkce ? { pkce: true } : {}),
957
1554
  ...(p.publicClient ? { publicClient: true } : {}),
958
1555
  ...(p.resource ? { resource: p.resource } : {}),
1556
+ ...(p.managed ? { managed: { connectorId: p.managed.connectorId, scopeSet: p.managed.scopeSet } } : {}),
1557
+ ...(p.account ? { account: p.account } : {}),
959
1558
  };
960
1559
  }
961
1560
 
962
- private requireProvider(p: unknown): OAuthProviderConfig {
1561
+ /**
1562
+ * @param opts.managed Accept a MANAGED provider (no client id of its own,
1563
+ * a connector reference instead). Only the managed sign-in paths pass it:
1564
+ * everywhere else a provider needs a client id, and a `managed` field a
1565
+ * caller slipped in is dropped rather than making the row one.
1566
+ */
1567
+ private requireProvider(p: unknown, opts: { managed?: boolean } = {}): OAuthProviderConfig {
963
1568
  if (!p || typeof p !== 'object') throw new InvalidSecretError('provider is required');
964
1569
  const cfg = p as Record<string, unknown>;
965
1570
  const authorizationUrl = this.requireUrl(cfg.authorizationUrl, 'authorizationUrl');
966
1571
  const tokenUrl = this.requireUrl(cfg.tokenUrl, 'tokenUrl');
967
- const clientId = typeof cfg.clientId === 'string' ? cfg.clientId.trim() : '';
968
- if (!clientId) throw new InvalidSecretError('clientId is required');
1572
+ const managed = opts.managed ? (this.managedRefOf(cfg) ?? undefined) : undefined;
1573
+ // A managed sign-in has no client of its own: whatever id a caller sends
1574
+ // beside the connector reference is not stored, so no row can claim one.
1575
+ const clientId = managed ? '' : typeof cfg.clientId === 'string' ? cfg.clientId.trim() : '';
1576
+ if (!clientId && !managed) throw new InvalidSecretError('clientId is required');
969
1577
  const scopes = Array.isArray(cfg.scopes) ? cfg.scopes.map(String) : undefined;
970
1578
  const authParams =
971
1579
  cfg.authParams && typeof cfg.authParams === 'object'
@@ -986,6 +1594,8 @@ export class DbSecretsVaultService implements ISecretsVaultService {
986
1594
  typeof cfg.resource === 'string' && cfg.resource.trim()
987
1595
  ? this.requireUrl(cfg.resource, 'resource')
988
1596
  : undefined,
1597
+ managed,
1598
+ account: typeof cfg.account === 'string' && cfg.account.trim() ? cfg.account.trim().slice(0, MAX_LABEL_LEN) : undefined,
989
1599
  };
990
1600
  }
991
1601