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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. package/agent-guide/access-control.md +19 -8
  2. package/agent-guide/html-views.md +231 -5
  3. package/agent-guide/tool-manuals.md +1 -1
  4. package/dist/core/catalog-revision.d.ts +12 -1
  5. package/dist/core/catalog-revision.d.ts.map +1 -1
  6. package/dist/core/catalog-revision.js +23 -3
  7. package/dist/core/catalog-revision.js.map +1 -1
  8. package/dist/core/core-ports.d.ts +41 -0
  9. package/dist/core/core-ports.d.ts.map +1 -1
  10. package/dist/core/core-ports.js.map +1 -1
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +14 -7
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +16 -5
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +37 -12
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +16 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -1
  20. package/dist/core/lifecycle.js +12 -2
  21. package/dist/core/lifecycle.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/modules/access/access-control.interface.d.ts +17 -9
  26. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  27. package/dist/modules/access/access-control.service.d.ts +12 -2
  28. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.js +164 -75
  30. package/dist/modules/access/access-control.service.js.map +1 -1
  31. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  32. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  33. package/dist/modules/access/access-mutation.service.js +48 -51
  34. package/dist/modules/access/access-mutation.service.js.map +1 -1
  35. package/dist/modules/access/access-view.d.ts.map +1 -1
  36. package/dist/modules/access/access-view.js +12 -11
  37. package/dist/modules/access/access-view.js.map +1 -1
  38. package/dist/modules/access/access.routes.d.ts +3 -0
  39. package/dist/modules/access/access.routes.d.ts.map +1 -1
  40. package/dist/modules/access/access.routes.js +35 -29
  41. package/dist/modules/access/access.routes.js.map +1 -1
  42. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  43. package/dist/modules/access/groups-admin.routes.js +4 -2
  44. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  45. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  46. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  47. package/dist/modules/access/groups-admin.service.js +80 -38
  48. package/dist/modules/access/groups-admin.service.js.map +1 -1
  49. package/dist/modules/access/groups-edit.d.ts +1 -1
  50. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  51. package/dist/modules/access/groups-edit.js +1 -1
  52. package/dist/modules/access/groups-edit.js.map +1 -1
  53. package/dist/modules/access/reference-scan.d.ts +116 -24
  54. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  55. package/dist/modules/access/reference-scan.js +305 -44
  56. package/dist/modules/access/reference-scan.js.map +1 -1
  57. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  58. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  59. package/dist/modules/access/roles-admin.service.js +32 -23
  60. package/dist/modules/access/roles-admin.service.js.map +1 -1
  61. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  62. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  63. package/dist/modules/access-model/access-grammar.js +60 -20
  64. package/dist/modules/access-model/access-grammar.js.map +1 -1
  65. package/dist/modules/access-model/access-splice.d.ts +22 -18
  66. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  67. package/dist/modules/access-model/access-splice.js +20 -14
  68. package/dist/modules/access-model/access-splice.js.map +1 -1
  69. package/dist/modules/access-model/group-files.d.ts +1 -0
  70. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  71. package/dist/modules/access-model/group-files.js +5 -1
  72. package/dist/modules/access-model/group-files.js.map +1 -1
  73. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  74. package/dist/modules/agent-guide/agent-guide.tools.js +13 -3
  75. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  76. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  77. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  78. package/dist/modules/agent-instructions/compose.js +3 -1
  79. package/dist/modules/agent-instructions/compose.js.map +1 -1
  80. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  81. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  82. package/dist/modules/agent-instructions/shared-file-rules.js +16 -1
  83. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  84. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  85. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  86. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  87. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  88. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  89. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  90. package/dist/modules/agent-scope/connection-scope.js +165 -0
  91. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  92. package/dist/modules/agent-scope/index.d.ts +3 -0
  93. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  94. package/dist/modules/agent-scope/index.js +3 -0
  95. package/dist/modules/agent-scope/index.js.map +1 -0
  96. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  97. package/dist/modules/audit/agent-audit.service.js +2 -0
  98. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  99. package/dist/modules/audit/audit.contract.d.ts +6 -0
  100. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  101. package/dist/modules/audit/audit.contract.js.map +1 -1
  102. package/dist/modules/auth/account-admission.d.ts +12 -0
  103. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  104. package/dist/modules/auth/account-admission.js +15 -0
  105. package/dist/modules/auth/account-admission.js.map +1 -1
  106. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  107. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  108. package/dist/modules/auth/auth.middleware.js +42 -4
  109. package/dist/modules/auth/auth.middleware.js.map +1 -1
  110. package/dist/modules/auth/auth.routes.d.ts +13 -9
  111. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  112. package/dist/modules/auth/auth.routes.js +35 -29
  113. package/dist/modules/auth/auth.routes.js.map +1 -1
  114. package/dist/modules/auth/auth.service.d.ts +55 -6
  115. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  116. package/dist/modules/auth/auth.service.js +128 -34
  117. package/dist/modules/auth/auth.service.js.map +1 -1
  118. package/dist/modules/auth/index.d.ts +1 -1
  119. package/dist/modules/auth/index.d.ts.map +1 -1
  120. package/dist/modules/auth/index.js +1 -1
  121. package/dist/modules/auth/index.js.map +1 -1
  122. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  123. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  124. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  125. package/dist/modules/database/core-schema.d.ts +68 -0
  126. package/dist/modules/database/core-schema.d.ts.map +1 -1
  127. package/dist/modules/database/core-schema.js +33 -0
  128. package/dist/modules/database/core-schema.js.map +1 -1
  129. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  130. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  131. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  132. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  133. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  134. package/dist/modules/embed/embed.tools.js +5 -0
  135. package/dist/modules/embed/embed.tools.js.map +1 -1
  136. package/dist/modules/kb-fs/index.d.ts +2 -0
  137. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  138. package/dist/modules/kb-fs/index.js +1 -0
  139. package/dist/modules/kb-fs/index.js.map +1 -1
  140. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  141. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  142. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  143. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  144. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  145. package/dist/modules/kb-fs/repo-path.js +22 -7
  146. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  147. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  148. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  149. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  150. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  153. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  154. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  155. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  156. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  157. package/dist/modules/mcp/mcp.routes.js +39 -4
  158. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  159. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  160. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  161. package/dist/modules/mcp/mcp.service.js +68 -13
  162. package/dist/modules/mcp/mcp.service.js.map +1 -1
  163. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  164. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  165. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  167. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  168. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  169. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  171. package/dist/modules/plugins/index.d.ts +1 -1
  172. package/dist/modules/plugins/index.d.ts.map +1 -1
  173. package/dist/modules/plugins/index.js.map +1 -1
  174. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  175. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-links.js.map +1 -1
  177. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  178. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  179. package/dist/modules/plugins/plugins.routes.js +32 -5
  180. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  181. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  182. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  183. package/dist/modules/secrets-vault/secrets-vault.routes.js +15 -3
  184. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  185. package/dist/modules/skills/index.d.ts +3 -3
  186. package/dist/modules/skills/index.d.ts.map +1 -1
  187. package/dist/modules/skills/index.js +0 -2
  188. package/dist/modules/skills/index.js.map +1 -1
  189. package/dist/modules/skills/skills.contract.d.ts +16 -0
  190. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  192. package/dist/modules/skills/skills.service.js +6 -1
  193. package/dist/modules/skills/skills.service.js.map +1 -1
  194. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  195. package/dist/modules/skills/skills.tools.js +24 -12
  196. package/dist/modules/skills/skills.tools.js.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  198. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  201. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  202. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  203. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  204. package/dist/modules/tool-auth/index.d.ts +1 -1
  205. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  206. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  207. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  208. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  209. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  210. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  211. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  212. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  213. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  214. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  215. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  216. package/dist/modules/tool-helpers/tool-context.js +3 -0
  217. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  218. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  219. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  220. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  221. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  222. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  223. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  224. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  225. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  226. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  227. package/dist/modules/tool-helpers/validate-token.js +1 -1
  228. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  229. package/dist/modules/tool-manuals/index.d.ts +3 -3
  230. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  231. package/dist/modules/tool-manuals/index.js +0 -2
  232. package/dist/modules/tool-manuals/index.js.map +1 -1
  233. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +19 -6
  234. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  235. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  236. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  238. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  239. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +13 -6
  240. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  241. package/dist/modules/tool-manuals/tool-manuals.service.js +23 -15
  242. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  243. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.tools.js +25 -10
  245. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  246. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  247. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  248. package/dist/modules/tool-registry/manual.routes.js +27 -7
  249. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  250. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  251. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  252. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  253. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  254. package/dist/modules/workflow/agent-tools/workflow.tools.js +14 -9
  255. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  256. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  257. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  258. package/dist/modules/workflow/git/git.service.js +20 -35
  259. package/dist/modules/workflow/git/git.service.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +3 -0
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  265. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  266. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  267. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  268. package/dist/modules/workspace/index.d.ts +8 -0
  269. package/dist/modules/workspace/index.d.ts.map +1 -1
  270. package/dist/modules/workspace/index.js +8 -0
  271. package/dist/modules/workspace/index.js.map +1 -1
  272. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  273. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  274. package/dist/modules/workspace/nul-scan.js +55 -0
  275. package/dist/modules/workspace/nul-scan.js.map +1 -0
  276. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  277. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  278. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  279. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  280. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  281. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  282. package/dist/modules/workspace/ripgrep-search.js +657 -0
  283. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  284. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  285. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  286. package/dist/modules/workspace/ripgrep-version.js +31 -0
  287. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  288. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  289. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  290. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  291. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  292. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  293. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  294. package/dist/modules/workspace/worker-pool.js.map +1 -1
  295. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  296. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  297. package/dist/modules/workspace/workspace-tmp.js +435 -0
  298. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  299. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  300. package/dist/modules/workspace/workspace.routes.js +5 -26
  301. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  302. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  303. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  304. package/dist/modules/workspace/workspace.service.js +86 -6
  305. package/dist/modules/workspace/workspace.service.js.map +1 -1
  306. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  307. package/dist/modules/workspace/workspace.tools.js +460 -116
  308. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  309. package/dist/shared/path-order.d.ts +28 -8
  310. package/dist/shared/path-order.d.ts.map +1 -1
  311. package/dist/shared/path-order.js +37 -8
  312. package/dist/shared/path-order.js.map +1 -1
  313. package/migrations/0022_session_version.sql +6 -0
  314. package/migrations/0023_agent_path.sql +3 -0
  315. package/migrations/meta/0022_snapshot.json +2360 -0
  316. package/migrations/meta/0023_snapshot.json +2378 -0
  317. package/migrations/meta/_journal.json +14 -0
  318. package/package.json +3 -3
  319. package/src/__tests__/pg-scratch.test.ts +69 -0
  320. package/src/__tests__/pg-scratch.ts +31 -0
  321. package/src/__tests__/slow-ripgrep.ts +65 -0
  322. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  323. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  324. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  325. package/src/core/__tests__/gated-boot.test.ts +8 -1
  326. package/src/core/__tests__/lifecycle.test.ts +46 -13
  327. package/src/core/catalog-revision.ts +24 -2
  328. package/src/core/core-ports.ts +42 -0
  329. package/src/core/create-core-server.ts +13 -0
  330. package/src/core/create-core-services.ts +49 -12
  331. package/src/core/lifecycle.ts +26 -2
  332. package/src/index.ts +1 -1
  333. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  334. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  335. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  336. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  337. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  338. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  339. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  340. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  341. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  342. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  343. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  344. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  345. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  346. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  347. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  348. package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -0
  349. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  350. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  351. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  352. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  353. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  354. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  355. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  356. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  357. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  358. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  359. package/src/modules/access/access-control.interface.ts +17 -10
  360. package/src/modules/access/access-control.service.ts +158 -72
  361. package/src/modules/access/access-mutation.service.ts +48 -53
  362. package/src/modules/access/access-view.ts +12 -10
  363. package/src/modules/access/access.routes.ts +43 -33
  364. package/src/modules/access/groups-admin.routes.ts +4 -2
  365. package/src/modules/access/groups-admin.service.ts +114 -44
  366. package/src/modules/access/groups-edit.ts +1 -1
  367. package/src/modules/access/reference-scan.ts +319 -46
  368. package/src/modules/access/roles-admin.service.ts +43 -26
  369. package/src/modules/access/synced-groups-writer.ts +1 -1
  370. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  371. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  372. package/src/modules/access-model/access-grammar.ts +65 -20
  373. package/src/modules/access-model/access-splice.ts +31 -23
  374. package/src/modules/access-model/group-files.ts +5 -0
  375. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +31 -0
  376. package/src/modules/agent-guide/agent-guide.tools.ts +13 -2
  377. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +1 -0
  378. package/src/modules/agent-instructions/compose.ts +3 -1
  379. package/src/modules/agent-instructions/shared-file-rules.ts +17 -1
  380. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  381. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  382. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  383. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  384. package/src/modules/agent-scope/connection-scope.ts +199 -0
  385. package/src/modules/agent-scope/index.ts +15 -0
  386. package/src/modules/audit/agent-audit.service.ts +2 -0
  387. package/src/modules/audit/audit.contract.ts +6 -0
  388. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  389. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  390. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  391. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  392. package/src/modules/auth/account-admission.ts +19 -0
  393. package/src/modules/auth/auth.middleware.ts +45 -4
  394. package/src/modules/auth/auth.routes.ts +46 -29
  395. package/src/modules/auth/auth.service.ts +135 -34
  396. package/src/modules/auth/index.ts +2 -0
  397. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  398. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  399. package/src/modules/database/core-schema.ts +33 -0
  400. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  401. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  402. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  403. package/src/modules/embed/embed.tools.ts +5 -0
  404. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  405. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  406. package/src/modules/kb-fs/index.ts +2 -0
  407. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  408. package/src/modules/kb-fs/repo-path.ts +33 -7
  409. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  410. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  411. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  412. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  413. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  414. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  415. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  416. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  417. package/src/modules/mcp/mcp.routes.ts +39 -3
  418. package/src/modules/mcp/mcp.service.ts +107 -17
  419. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  420. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  421. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  422. package/src/modules/plugins/index.ts +1 -1
  423. package/src/modules/plugins/plugin-links.ts +19 -2
  424. package/src/modules/plugins/plugins.routes.ts +35 -4
  425. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  426. package/src/modules/secrets-vault/secrets-vault.routes.ts +23 -3
  427. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  428. package/src/modules/skills/index.ts +7 -2
  429. package/src/modules/skills/skills.contract.ts +17 -0
  430. package/src/modules/skills/skills.service.ts +5 -1
  431. package/src/modules/skills/skills.tools.ts +44 -13
  432. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  433. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  434. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  435. package/src/modules/tool-auth/index.ts +1 -1
  436. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  437. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  438. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  439. package/src/modules/tool-helpers/tool-context.ts +10 -0
  440. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  441. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  442. package/src/modules/tool-helpers/validate-token.ts +4 -2
  443. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  444. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  445. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  446. package/src/modules/tool-manuals/index.ts +6 -2
  447. package/src/modules/tool-manuals/tool-manuals.contract.ts +18 -6
  448. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  449. package/src/modules/tool-manuals/tool-manuals.service.ts +31 -15
  450. package/src/modules/tool-manuals/tool-manuals.tools.ts +28 -11
  451. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +2 -2
  452. package/src/modules/tool-registry/manual.routes.ts +26 -5
  453. package/src/modules/tool-registry/tool.contract.ts +7 -0
  454. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  455. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  457. package/src/modules/workflow/agent-tools/workflow.tools.ts +14 -9
  458. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  459. package/src/modules/workflow/git/git.service.ts +29 -48
  460. package/src/modules/workflow/workflow.service.ts +5 -0
  461. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  462. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  463. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  464. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  465. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  466. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  467. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  468. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  469. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -2
  470. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  471. package/src/modules/workspace/__tests__/workspace.tools.test.ts +393 -113
  472. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  473. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  474. package/src/modules/workspace/index.ts +20 -0
  475. package/src/modules/workspace/nul-scan.ts +50 -0
  476. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  477. package/src/modules/workspace/ripgrep-search.ts +715 -0
  478. package/src/modules/workspace/ripgrep-version.ts +35 -0
  479. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  480. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  481. package/src/modules/workspace/worker-pool.ts +3 -4
  482. package/src/modules/workspace/workspace-tmp.ts +457 -0
  483. package/src/modules/workspace/workspace.routes.ts +6 -27
  484. package/src/modules/workspace/workspace.service.ts +92 -15
  485. package/src/modules/workspace/workspace.tools.ts +500 -155
  486. package/src/shared/path-order.ts +37 -8
  487. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  488. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  489. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  490. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  491. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  492. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  493. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  494. package/dist/modules/workspace/bounded-matcher.js +0 -200
  495. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  496. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  497. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -9,6 +9,7 @@ import {
9
9
  AccountChangeRefusedError,
10
10
  AccountDeactivatedError,
11
11
  AuthBackendError,
12
+ SessionEndedError,
12
13
  admitEveryone,
13
14
  type AccountProvisionReason,
14
15
  type IAccountAdmission,
@@ -32,6 +33,8 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
32
33
  * request carrying a credential asks, so the answer is kept briefly rather
33
34
  * than read each time; switching an account off or on in THIS process
34
35
  * forgets it at once, and another replica sees the change within this long.
36
+ * A session token never waits on it: {@link AuthService.resolveSession}
37
+ * reads the row each time, so a password change ends it everywhere at once.
35
38
  */
36
39
  const ACTIVE_CACHE_MS = 30_000;
37
40
 
@@ -69,6 +72,33 @@ export const OWNER_SWITCH_OFF_REFUSAL = "The owner can't be switched off.";
69
72
  /** What an admin is told when deleting an owner's account would leave them no way to sign in. */
70
73
  export const OWNER_DELETE_REFUSAL = "The owner's account can't be deleted: they would have no way to sign in.";
71
74
 
75
+ /**
76
+ * A password change refused because the password it proved is no longer the
77
+ * account's: another session changed it between this change's read and its
78
+ * write. Nothing was written; the routes answer it with a 409 and its words.
79
+ */
80
+ export class PasswordChangedMeanwhileError extends Error {
81
+ constructor() {
82
+ super('Your password was changed from another session while this change was being made, so nothing was changed. Sign in with the new password.');
83
+ this.name = 'PasswordChangedMeanwhileError';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * A password write the service refuses on purpose: the policy, a wrong
89
+ * current password, an account whose password is not kept here. The message
90
+ * is written for the caller, and a route answers it with a 400 — unlike any
91
+ * other error a password write raises, which is a fault and answered generic.
92
+ */
93
+ export class PasswordRefusedError extends Error {
94
+ readonly status = 400;
95
+
96
+ constructor(message: string) {
97
+ super(message);
98
+ this.name = 'PasswordRefusedError';
99
+ }
100
+ }
101
+
72
102
  /**
73
103
  * Decoy hash verified when the email is unknown or has no password set, so
74
104
  * those paths cost the same scrypt work as a real wrong-password attempt —
@@ -271,7 +301,7 @@ export class AuthService implements IAccountSwitchGuard {
271
301
  const defaultName = normalizedEmail.split('@')[0] || normalizedEmail;
272
302
  await this.assertAdmitted(normalizedEmail, 'bootstrap');
273
303
  const admin = await this.clearOwnerSwitchOff(await this.upsertUserByEmail(normalizedEmail, defaultName));
274
- return { token: this.signToken(admin.id, admin.email), user: this.toClientUser(admin) };
304
+ return { token: this.signToken(admin), user: this.toClientUser(admin) };
275
305
  }
276
306
 
277
307
  // Always run one scrypt verification — against the stored hash when there
@@ -287,7 +317,7 @@ export class AuthService implements IAccountSwitchGuard {
287
317
  // is switched off would tell them it has an account.
288
318
  const signedIn = await this.clearOwnerSwitchOff(user);
289
319
  if (signedIn.deactivatedAt) throw new AccountDeactivatedError();
290
- return { token: this.signToken(signedIn.id, signedIn.email), user: this.toClientUser(signedIn) };
320
+ return { token: this.signToken(signedIn), user: this.toClientUser(signedIn) };
291
321
  }
292
322
 
293
323
  /**
@@ -319,7 +349,7 @@ export class AuthService implements IAccountSwitchGuard {
319
349
  const displayName = (name ?? '').trim() || normalizedEmail.split('@')[0] || normalizedEmail;
320
350
  const user = await this.clearOwnerSwitchOff(await this.upsertUserByEmail(normalizedEmail, displayName));
321
351
  if (user.deactivatedAt) throw new AccountDeactivatedError();
322
- return { token: this.signToken(user.id, user.email), user: this.toClientUser(user) };
352
+ return { token: this.signToken(user), user: this.toClientUser(user) };
323
353
  }
324
354
 
325
355
  /**
@@ -361,7 +391,7 @@ export class AuthService implements IAccountSwitchGuard {
361
391
  // than sending the admin off to pick a longer password first.
362
392
  this.assertNotSystemAccount(normalizedEmail);
363
393
  if (this.isEnvAdminEmail(normalizedEmail)) {
364
- throw new Error(ENV_ADMIN_PASSWORD_REFUSAL);
394
+ throw new PasswordRefusedError(ENV_ADMIN_PASSWORD_REFUSAL);
365
395
  }
366
396
  this.assertPasswordPolicy(password);
367
397
  await this.assertAdmitted(normalizedEmail, 'admin-create');
@@ -370,16 +400,22 @@ export class AuthService implements IAccountSwitchGuard {
370
400
  // EXPLICITLY supplied name is persisted too; a blank name keeps the
371
401
  // existing display name rather than clobbering it with the local-part
372
402
  // fallback. `returning()` yields the authoritative row either way.
403
+ // A reset is a password change like any other: the session generation
404
+ // moves on with it, so every session opened with the old password ends,
405
+ // and a self-service change that read the row before it is refused
406
+ // rather than overwriting the reset (see {@link changePassword}).
407
+ const sessionVersion = sql`${users.sessionVersion} + 1`;
373
408
  const [row] = await this.db
374
409
  .insert(users)
375
410
  .values({ email: normalizedEmail, emailBidx: normalizedEmail, name: displayName, passwordHash })
376
411
  .onConflictDoUpdate({
377
412
  target: users.emailBidx,
378
413
  set: suppliedName
379
- ? { passwordHash, name: suppliedName, updatedAt: new Date() }
380
- : { passwordHash, updatedAt: new Date() },
414
+ ? { passwordHash, sessionVersion, name: suppliedName, updatedAt: new Date() }
415
+ : { passwordHash, sessionVersion, updatedAt: new Date() },
381
416
  })
382
417
  .returning();
418
+ this.activeCache.delete(row.id);
383
419
  return this.toClientUser(row);
384
420
  }
385
421
 
@@ -443,21 +479,26 @@ export class AuthService implements IAccountSwitchGuard {
443
479
  if (existing) {
444
480
  return { user: this.toClientUser(existing), created: false, passwordSet: false, deactivated: existing.deactivatedAt != null };
445
481
  }
446
- throw new Error(ENV_ADMIN_PASSWORD_REFUSAL);
482
+ throw new PasswordRefusedError(ENV_ADMIN_PASSWORD_REFUSAL);
447
483
  }
448
484
  this.assertPasswordPolicy(password);
449
485
  await this.assertAdmitted(normalizedEmail, 'admin-create');
450
486
  const passwordHash = await hashPassword(password);
451
487
  const displayName = normalizedEmail.split('@')[0] || normalizedEmail;
488
+ // A first password on an existing account is a password write like any
489
+ // other, so the session generation moves on with it: a self-service
490
+ // first password that read the row before this one is refused rather
491
+ // than overwriting it (see {@link changePassword}).
452
492
  const [row] = await this.db
453
493
  .insert(users)
454
494
  .values({ email: normalizedEmail, emailBidx: normalizedEmail, name: displayName, passwordHash })
455
495
  .onConflictDoUpdate({
456
496
  target: users.emailBidx,
457
- set: { passwordHash, updatedAt: new Date() },
497
+ set: { passwordHash, sessionVersion: sql`${users.sessionVersion} + 1`, updatedAt: new Date() },
458
498
  setWhere: and(isNull(users.passwordHash), isNull(users.deactivatedAt)),
459
499
  })
460
500
  .returning({ ...getTableColumns(users), created: CREATED });
501
+ if (row) this.activeCache.delete(row.id);
461
502
  if (row) return { user: this.toClientUser(row), created: row.created === true, passwordSet: true, deactivated: false };
462
503
  // The conflicting account has a password or is switched off: untouched.
463
504
  const [existing] = await this.db.select().from(users).where(eq(users.emailBidx, normalizedEmail)).limit(1);
@@ -586,31 +627,49 @@ export class AuthService implements IAccountSwitchGuard {
586
627
  * environment password is the platform's rescue path into a deployment and
587
628
  * is changed there, which is what the Account page says in place of the
588
629
  * form.
630
+ *
631
+ * A change ends every session opened before it: the row's
632
+ * `sessionVersion` moves on in the same write, so every token issued
633
+ * before it is refused at its next request (see {@link resolveSession}).
634
+ * The caller's own session is the one exception: the token returned here
635
+ * carries the new generation and is what the caller keeps using.
636
+ *
637
+ * The write only lands on the row as it was read. If another session
638
+ * changed the password between the read and the write, the current
639
+ * password this one proved is no longer current, so it is refused with
640
+ * {@link PasswordChangedMeanwhileError} and nothing is written. Two clicks
641
+ * of one form are the same race.
589
642
  */
590
643
  async changePassword(
591
644
  userId: string,
592
645
  currentPassword: string | undefined,
593
646
  newPassword: string,
594
- ): Promise<void> {
647
+ ): Promise<{ token: string }> {
595
648
  const [user] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
596
- if (!user) throw new Error('User not found');
649
+ if (!user) throw new PasswordRefusedError('User not found');
597
650
  // Before the policy check, so the deployment admin is told the real reason
598
651
  // rather than being sent to fix a password that would be refused anyway.
599
652
  this.assertNotSystemAccount(user.email);
600
653
  if (this.isEnvAdminEmail(user.email)) {
601
- throw new Error(ENV_ADMIN_PASSWORD_REFUSAL);
654
+ throw new PasswordRefusedError(ENV_ADMIN_PASSWORD_REFUSAL);
602
655
  }
603
656
  this.assertPasswordPolicy(newPassword);
604
657
  if (user.passwordHash) {
605
658
  if (!currentPassword || !(await verifyPassword(currentPassword, user.passwordHash))) {
606
- throw new Error('Current password is incorrect');
659
+ throw new PasswordRefusedError('Current password is incorrect');
607
660
  }
608
661
  }
609
662
  const passwordHash = await hashPassword(newPassword);
610
- await this.db
663
+ const [changed] = await this.db
611
664
  .update(users)
612
- .set({ passwordHash, updatedAt: new Date() })
613
- .where(eq(users.id, userId));
665
+ .set({ passwordHash, sessionVersion: sql`${users.sessionVersion} + 1`, updatedAt: new Date() })
666
+ .where(and(eq(users.id, userId), eq(users.sessionVersion, user.sessionVersion)))
667
+ .returning();
668
+ if (!changed) throw new PasswordChangedMeanwhileError();
669
+ // Sessions read the row fresh (see resolveSession); the cached entry is
670
+ // dropped so nothing in this process keeps the old generation either.
671
+ this.activeCache.delete(userId);
672
+ return { token: this.signToken(changed) };
614
673
  }
615
674
 
616
675
  /**
@@ -664,18 +723,24 @@ export class AuthService implements IAccountSwitchGuard {
664
723
  /** Every password write calls this first: the system accounts never get one. */
665
724
  private assertNotSystemAccount(normalizedEmail: string): void {
666
725
  if (SYSTEM_ACCOUNT_EMAILS.includes(normalizedEmail)) {
667
- throw new Error(SYSTEM_ACCOUNT_PASSWORD_REFUSAL);
726
+ throw new PasswordRefusedError(SYSTEM_ACCOUNT_PASSWORD_REFUSAL);
668
727
  }
669
728
  }
670
729
 
671
730
  private assertPasswordPolicy(password: string): void {
672
731
  if ((password ?? '').length < MIN_PASSWORD_LENGTH) {
673
- throw new Error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
732
+ throw new PasswordRefusedError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
674
733
  }
675
734
  }
676
735
 
677
- private signToken(userId: string, email: string): string {
678
- return jwt.sign({ userId, email }, this.config.jwtSecret, { expiresIn: '7d' });
736
+ /**
737
+ * A session token for `user`, stamped with the session generation its row
738
+ * is at (`sv`): {@link resolveSession} refuses it once the row moves on.
739
+ */
740
+ private signToken(user: { id: string; email: string; sessionVersion: number }): string {
741
+ return jwt.sign({ userId: user.id, email: user.email, sv: user.sessionVersion }, this.config.jwtSecret, {
742
+ expiresIn: '7d',
743
+ });
679
744
  }
680
745
 
681
746
  /**
@@ -710,39 +775,55 @@ export class AuthService implements IAccountSwitchGuard {
710
775
 
711
776
  /**
712
777
  * The signature and expiry of a session token, and nothing else — whether
713
- * its account is still on is {@link resolveSession}'s question. Every
778
+ * its account is still on, and whether the session is still honoured, are
779
+ * {@link resolveSession}'s questions. Every
714
780
  * place a session is ACCEPTED goes through that one.
715
781
  */
716
782
  verifyToken(token: string): { userId: string; email: string } {
783
+ const { userId, email } = this.decodeSession(token);
784
+ return { userId, email };
785
+ }
786
+
787
+ /** {@link verifyToken} plus the session generation the token was issued at. */
788
+ private decodeSession(token: string): { userId: string; email: string; sessionVersion: number } {
717
789
  const decoded = jwt.verify(token, this.config.jwtSecret) as {
718
790
  userId: string;
719
791
  email: string;
792
+ sv?: number;
720
793
  };
721
- return { userId: decoded.userId, email: decoded.email };
794
+ // A token issued before sessions had generations carries none: it is of
795
+ // the first one, which is where every row started.
796
+ return { userId: decoded.userId, email: decoded.email, sessionVersion: decoded.sv ?? 0 };
722
797
  }
723
798
 
724
799
  /**
725
- * A session token that may still be used: signed by us, not expired, and
726
- * its account still on. A token outlives a deactivation by up to seven
800
+ * A session token that may still be used: signed by us, not expired, its
801
+ * account still on, and issued since the account's password last changed.
802
+ * A token outlives a deactivation or a password change by up to seven
727
803
  * days, so the signature alone no longer says the bearer may enter.
728
- * Throws for an invalid token, {@link AccountDeactivatedError} for one
729
- * whose account was switched off.
804
+ * Throws for an invalid token, {@link SessionEndedError} for one a
805
+ * password change ended, {@link AccountDeactivatedError} for one whose
806
+ * account was switched off.
730
807
  */
731
808
  async resolveSession(token: string): Promise<{ userId: string; email: string }> {
732
- const claim = this.verifyToken(token);
733
- let active: boolean;
809
+ const { sessionVersion, ...claim } = this.decodeSession(token);
810
+ let account: { active: boolean; sessionVersion: number };
734
811
  try {
735
- active = await this.isActive(claim.userId);
812
+ // Read fresh, never from the cache: a password changed on another
813
+ // replica must end this token at its very next request, and only the
814
+ // row can say it was.
815
+ account = await this.accountState(claim.userId, { fresh: true });
736
816
  } catch (err) {
737
817
  // The token is fine; the database is not. Said apart from a refusal, so
738
818
  // an outage is a 500 and not a sign-out (see `AuthBackendError`).
739
819
  throw new AuthBackendError(err);
740
820
  }
741
- if (!active) throw new AccountDeactivatedError();
821
+ if (!account.active) throw new AccountDeactivatedError();
822
+ if (sessionVersion !== account.sessionVersion) throw new SessionEndedError();
742
823
  return claim;
743
824
  }
744
825
 
745
- private readonly activeCache = new Map<string, { active: boolean; at: number }>();
826
+ private readonly activeCache = new Map<string, { active: boolean; sessionVersion: number; at: number }>();
746
827
 
747
828
  /**
748
829
  * Whether the account behind a credential is on: it exists and no admin
@@ -762,18 +843,38 @@ export class AuthService implements IAccountSwitchGuard {
762
843
  * ({@link clearOwnerSwitchOff}).
763
844
  */
764
845
  async isActive(userId: string): Promise<boolean> {
846
+ return (await this.accountState(userId)).active;
847
+ }
848
+
849
+ /**
850
+ * {@link isActive}'s answer and the session generation the account is at,
851
+ * read and cached together; a password change in this process forgets the
852
+ * entry at once, as switching an account off does. `fresh` reads the row
853
+ * whatever the cache holds (and refreshes it): {@link resolveSession}'s
854
+ * question, since a session's generation must hold on every replica.
855
+ */
856
+ private async accountState(
857
+ userId: string,
858
+ { fresh = false }: { fresh?: boolean } = {},
859
+ ): Promise<{ active: boolean; sessionVersion: number }> {
765
860
  const now = Date.now();
766
861
  const hit = this.activeCache.get(userId);
767
- if (hit && now - hit.at < ACTIVE_CACHE_MS) return hit.active;
862
+ if (!fresh && hit && now - hit.at < ACTIVE_CACHE_MS) return hit;
768
863
  const [found] = await this.db
769
- .select({ id: users.id, email: users.email, deactivatedAt: users.deactivatedAt })
864
+ .select({
865
+ id: users.id,
866
+ email: users.email,
867
+ deactivatedAt: users.deactivatedAt,
868
+ sessionVersion: users.sessionVersion,
869
+ })
770
870
  .from(users)
771
871
  .where(eq(users.id, userId))
772
872
  .limit(1);
773
873
  const row = found && (await this.clearOwnerSwitchOff(found));
774
874
  const active = row !== undefined && (!row.deactivatedAt || this.isEnvAdminEmail(row.email ?? ''));
775
- this.activeCache.set(userId, { active, at: now });
776
- return active;
875
+ const entry = { active, sessionVersion: row?.sessionVersion ?? 0, at: now };
876
+ this.activeCache.set(userId, entry);
877
+ return entry;
777
878
  }
778
879
 
779
880
  /**
@@ -15,6 +15,8 @@ export {
15
15
  AccountChangeRefusedError,
16
16
  AccountDeactivatedError,
17
17
  AuthBackendError,
18
+ SESSION_ENDED_MESSAGE,
19
+ SessionEndedError,
18
20
  admitEveryone,
19
21
  type AccountAdmissionVerdict,
20
22
  type AccountProvisionReason,
@@ -4,8 +4,7 @@ import { logger } from '../../shared/logging.js';
4
4
  const log = logger('oidc');
5
5
  import type express from 'express';
6
6
  import type { AuthProviderPlugin } from './auth.routes.js';
7
- import { AUTH_COOKIE_MAX_AGE_S } from './auth.routes.js';
8
- import { AUTH_COOKIE_NAME } from './auth.middleware.js';
7
+ import { setAuthCookie } from './auth.middleware.js';
9
8
  import type { AuthService } from './auth.service.js';
10
9
  import { AccountAdmissionRefusedError, AccountDeactivatedError } from './account-admission.js';
11
10
  import { normalizeIssuerUrl } from '../settings/index.js';
@@ -311,13 +310,7 @@ export class OidcAuthProvider implements AuthProviderPlugin {
311
310
  } catch (error) {
312
311
  log.error('OIDC sign-in record failed:', { err: error });
313
312
  }
314
- res.cookie(AUTH_COOKIE_NAME, token, {
315
- httpOnly: true,
316
- sameSite: 'lax',
317
- secure: cookieSecure,
318
- maxAge: AUTH_COOKIE_MAX_AGE_S * 1000,
319
- path: '/',
320
- });
313
+ setAuthCookie(res, token, cookieSecure);
321
314
  res.redirect(`${publicFrontendUrl}/auth/oidc/callback#token=${encodeURIComponent(token)}`);
322
315
  } catch (error) {
323
316
  // A refused provisioning is not a failed sign-in: the identity was
@@ -0,0 +1,133 @@
1
+ import { afterAll, describe, expect, it } from 'vitest';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { migrate } from 'drizzle-orm/node-postgres/migrator';
7
+ import { sql } from 'drizzle-orm';
8
+ import { closeDb, createDb, type Database } from '../connection.js';
9
+ import { runCoreMigrations, runPiiEncryptionBackfill } from '../migrate.js';
10
+ import { coreMigrationsDir } from '../../../assets.js';
11
+ import { IndexOnWrite, SealOnWrite } from '../../../shared/column-crypto.js';
12
+ import { PG_ADMIN_URL, PG_TEST_TIMEOUT, dropScratchDatabases, scratchDatabaseUrl } from '../../../__tests__/pg-scratch.js';
13
+
14
+ /**
15
+ * Migration 0022 on a real Postgres, from both states a deployment can be in
16
+ * when it starts on this release: without `users.session_version` (every
17
+ * account starts at 0, so nobody is signed out by the upgrade), and with the
18
+ * column already there (the guarded `ADD COLUMN` changes nothing, so the
19
+ * generations it holds, and the sessions they honour, survive the start).
20
+ *
21
+ * Runs where `TEST_DATABASE_URL` names a server this role may create
22
+ * databases on, each test in a scratch database of its own.
23
+ */
24
+ const KEY = randomBytes(32).toString('base64');
25
+ const open: Database[] = [];
26
+ const folders: string[] = [];
27
+
28
+ /** The core history as it stood BEFORE 0022: the packaged folder minus 0022, in a folder of this test's own. */
29
+ function historyBefore0022(): string {
30
+ const folder = mkdtempSync(path.join(tmpdir(), 'hexis-core-before-0022-'));
31
+ folders.push(folder);
32
+ cpSync(coreMigrationsDir(), folder, { recursive: true });
33
+ rmSync(path.join(folder, '0022_session_version.sql'));
34
+ rmSync(path.join(folder, 'meta', '0022_snapshot.json'));
35
+ const journalPath = path.join(folder, 'meta', '_journal.json');
36
+ const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as { entries: { idx: number }[] };
37
+ journal.entries = journal.entries.filter((e) => e.idx < 22);
38
+ writeFileSync(journalPath, JSON.stringify(journal, null, 2));
39
+ return folder;
40
+ }
41
+
42
+ /**
43
+ * A database an older version left: started on the history to 0021, holding
44
+ * the accounts that version wrote, through SQL that names only the columns
45
+ * it knew. `withColumn` adds `session_version` by hand first, as a database
46
+ * that already has it would, and gives each account a generation of its own.
47
+ */
48
+ async function olderDeployment(withColumn: boolean): Promise<Database> {
49
+ const db = createDb(await scratchDatabaseUrl('hexis_session_version'), { piiKey: KEY });
50
+ open.push(db);
51
+ await migrate(db, { migrationsFolder: historyBefore0022(), migrationsTable: '__drizzle_migrations_core' });
52
+ await runPiiEncryptionBackfill(db);
53
+ if (withColumn) await db.execute(sql`alter table users add column session_version integer default 0 not null`);
54
+ const seal = (plain: string) => new SealOnWrite(plain) as unknown as string;
55
+ const index = (plain: string) => new IndexOnWrite(plain) as unknown as string;
56
+ const account = (email: string, name: string) => sql`(${seal(email)}, ${index(email)}, ${seal(name)})`;
57
+ await db.execute(sql`
58
+ insert into users (email, email_bidx, name) values
59
+ ${account('ada@example.com', 'Ada')},
60
+ ${account('bo@example.com', 'Bo')}
61
+ `);
62
+ if (withColumn) {
63
+ await db.execute(sql`update users set session_version = 3 where email_bidx = ${index('ada@example.com')}`);
64
+ await db.execute(sql`update users set session_version = 7 where email_bidx = ${index('bo@example.com')}`);
65
+ }
66
+ return db;
67
+ }
68
+
69
+ /** How many migrations the packaged history holds — what a fresh start records as applied. */
70
+ function packagedHistoryLength(): number {
71
+ const journal = JSON.parse(readFileSync(path.join(coreMigrationsDir(), 'meta', '_journal.json'), 'utf8')) as {
72
+ entries: unknown[];
73
+ };
74
+ return journal.entries.length;
75
+ }
76
+
77
+ /** Each account's generation, in a fixed order. */
78
+ const generations = async (db: Database) =>
79
+ (await db.execute(sql`select session_version from users order by session_version`)).rows.map(
80
+ (r) => (r as { session_version: number }).session_version,
81
+ );
82
+
83
+ /** What the column is: its type, default and nullability, as Postgres reports them. */
84
+ const columnShape = async (db: Database) =>
85
+ (
86
+ await db.execute(sql`
87
+ select data_type, column_default, is_nullable from information_schema.columns
88
+ where table_schema = 'public' and table_name = 'users' and column_name = 'session_version'
89
+ `)
90
+ ).rows;
91
+
92
+ const EXPECTED_SHAPE = [{ data_type: 'integer', column_default: '0', is_nullable: 'NO' }];
93
+
94
+ describe.skipIf(!PG_ADMIN_URL)('migration 0022 (users.session_version), on a real Postgres', () => {
95
+ afterAll(async () => {
96
+ for (const db of open.splice(0)) await closeDb(db);
97
+ for (const folder of folders.splice(0)) rmSync(folder, { recursive: true, force: true });
98
+ await dropScratchDatabases();
99
+ }, PG_TEST_TIMEOUT);
100
+
101
+ it(
102
+ 'adds the column at 0 for every existing account, so no session is ended by the upgrade',
103
+ async () => {
104
+ const db = await olderDeployment(false);
105
+ expect(await columnShape(db)).toEqual([]);
106
+
107
+ await runCoreMigrations(db, coreMigrationsDir());
108
+
109
+ expect(await columnShape(db)).toEqual(EXPECTED_SHAPE);
110
+ expect(await generations(db)).toEqual([0, 0]);
111
+ },
112
+ PG_TEST_TIMEOUT,
113
+ );
114
+
115
+ it(
116
+ 'finds the column already there and changes nothing, keeping every generation it holds',
117
+ async () => {
118
+ const db = await olderDeployment(true);
119
+ expect(await columnShape(db)).toEqual(EXPECTED_SHAPE);
120
+
121
+ await runCoreMigrations(db, coreMigrationsDir());
122
+
123
+ expect(await columnShape(db)).toEqual(EXPECTED_SHAPE);
124
+ expect(await generations(db)).toEqual([3, 7]);
125
+ // And 0022 is recorded as applied, so the next start does not try it
126
+ // again — judged against the packaged history's own length, so a later
127
+ // migration does not make this count stale.
128
+ const applied = await db.execute(sql`select count(*)::int as n from drizzle.__drizzle_migrations_core`);
129
+ expect((applied.rows[0] as { n: number }).n).toBe(packagedHistoryLength());
130
+ },
131
+ PG_TEST_TIMEOUT,
132
+ );
133
+ });
@@ -41,6 +41,16 @@ export const users = pgTable('users', {
41
41
  * until an admin (or the user, from their Account page) sets one.
42
42
  */
43
43
  passwordHash: text('password_hash'),
44
+ /**
45
+ * Which generation of this account's sessions is honoured. Every session
46
+ * token carries the value it was issued at (`sv`; a token from before the
47
+ * column existed carries none and counts as 0), and one whose value is not
48
+ * the row's is refused. Changing the password moves it on, so every session
49
+ * opened with the old password ends at its next request — the one that
50
+ * made the change is handed a fresh token. Connection keys are not
51
+ * sessions and do not look at it.
52
+ */
53
+ sessionVersion: integer('session_version').default(0).notNull(),
44
54
  /**
45
55
  * The one onboarding fact the server keeps: has this person concluded the
46
56
  * connect-your-agent setup (the welcome page's Done, or the reminder
@@ -277,6 +287,15 @@ export const externalApiKeys = pgTable('api_tokens', {
277
287
  * deleted in one step.
278
288
  */
279
289
  deletedAt: timestamp('deleted_at'),
290
+ /**
291
+ * The agent configuration the key connects as — a repo-relative knowledge-base
292
+ * path, chosen when the key is minted. Null is Everything: the key sees
293
+ * every tool, skill and plugin its owner may read, exactly as before the
294
+ * column existed. The path is the whole record: the file is re-read on
295
+ * every surface build, so editing it changes what the key sees without a
296
+ * new key (see `modules/agent-scope`).
297
+ */
298
+ agentPath: text('agent_path'),
280
299
  }, (t) => ({
281
300
  byUser: index('api_tokens_by_user').on(t.userId),
282
301
  }));
@@ -486,6 +505,12 @@ export const oauthAuthCodes = pgTable('oauth_auth_codes', {
486
505
  expiresAt: timestamp('expires_at').notNull(),
487
506
  consumedAt: timestamp('consumed_at'),
488
507
  createdAt: timestamp('created_at').defaultNow().notNull(),
508
+ /**
509
+ * The "Connect as" choice made on the consent step, carried from Finish to
510
+ * the token exchange that makes (or finds) the agent connection — the code
511
+ * is the only thing that travels between the two. Null is Everything.
512
+ */
513
+ agentPath: text('agent_path'),
489
514
  }, (t) => ({
490
515
  // Sweep expired/spent rows without a full scan.
491
516
  byExpiry: index('oauth_auth_codes_by_expiry').on(t.expiresAt),
@@ -525,6 +550,14 @@ export const agentConnections = pgTable('agent_connections', {
525
550
  revokedAt: timestamp('revoked_at'),
526
551
  /** `owner` | `admin` — who ended it; null while live. Same vocabulary as `api_tokens.revoked_by`. */
527
552
  revokedBy: text('revoked_by'),
553
+ /**
554
+ * The agent configuration the person chose to connect as on the consent step — a
555
+ * repo-relative knowledge-base path — or null for Everything. Set by every
556
+ * consent that lands on the row, so re-authorising with another choice
557
+ * changes what the agent sees from its next request. Same meaning as
558
+ * `api_tokens.agent_path`.
559
+ */
560
+ agentPath: text('agent_path'),
528
561
  }, (t) => ({
529
562
  byUser: index('agent_connections_by_user').on(t.userId),
530
563
  liveUnq: uniqueIndex('agent_connections_live_unq')
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
4
4
  import { createDeclaredVariableRoutes } from '../declared-variables.routes.js';
5
5
  import type { IToolManualService, ToolManualSummary } from '../../tool-manuals/tool-manuals.contract.js';
6
6
  import type { ISecretsVaultService } from '../../secrets-vault/secrets-vault.contract.js';
7
+ import { ConnectionScope, type IAgentScopeResolver } from '../../agent-scope/index.js';
7
8
 
8
9
  /**
9
10
  * The one core route that releases secret VALUES. What is worth pinning here is
@@ -46,6 +47,9 @@ async function mount(opts: {
46
47
  source?: 'session' | 'external' | 'internal';
47
48
  manuals?: ToolManualSummary[];
48
49
  secrets?: Record<string, string>;
50
+ /** The `.agent` file the caller's connection was made as; absent is Everything. */
51
+ agentPath?: string;
52
+ agentScope?: Pick<IAgentScopeResolver, 'resolve'>;
49
53
  }): Promise<Harness> {
50
54
  const userId = opts.userId ?? 'user-1';
51
55
  const source = opts.source ?? 'external';
@@ -64,10 +68,18 @@ async function mount(opts: {
64
68
  toolManualService,
65
69
  secretsVault,
66
70
  (req, _res, next) => {
67
- if (userId) req.toolAuth = { userId, source, scope: 'write' } as typeof req.toolAuth;
71
+ if (userId) {
72
+ req.toolAuth = {
73
+ userId,
74
+ source,
75
+ scope: 'write',
76
+ ...(opts.agentPath ? { agentPath: opts.agentPath } : {}),
77
+ } as typeof req.toolAuth;
78
+ }
68
79
  next();
69
80
  },
70
81
  async () => 'runner@x.eu',
82
+ opts.agentScope,
71
83
  ),
72
84
  );
73
85
  httpServer = await new Promise<HttpServer>((r) => {
@@ -133,6 +145,37 @@ describe('POST /agent/local-tools/:slug/variables', () => {
133
145
  expect(resolve).not.toHaveBeenCalled();
134
146
  });
135
147
 
148
+ it("releases only the variables of a connector the connection's .agent file loads", async () => {
149
+ const agent = { path: 'Agents/ops.agent', name: 'Ops' };
150
+ const loading = (connectors: string[]) => ({
151
+ resolve: async () => ConnectionScope.narrowed(agent, { skills: [], connectors, platformTools: null }, 'fp'),
152
+ });
153
+ // A folder entry over the tool's plugin loads it.
154
+ const inScope = await mount({
155
+ manuals: [GIT_TOOL],
156
+ secrets: { git_GITHUB_TOKEN: 'ghp_secret' },
157
+ agentPath: agent.path,
158
+ agentScope: loading(['Plugins/Engineering']),
159
+ });
160
+ expect((await fetch(`${inScope.base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(200);
161
+ await new Promise<void>((r) => httpServer!.close(() => r()));
162
+
163
+ // A readable tool the file does not load is as absent as an unreadable one.
164
+ const outOfScope = await mount({
165
+ manuals: [GIT_TOOL],
166
+ secrets: { git_GITHUB_TOKEN: 'ghp_secret' },
167
+ agentPath: agent.path,
168
+ agentScope: loading(['Plugins/billing.tool']),
169
+ });
170
+ expect((await fetch(`${outOfScope.base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(404);
171
+ expect(outOfScope.resolve).not.toHaveBeenCalled();
172
+ await new Promise<void>((r) => httpServer!.close(() => r()));
173
+
174
+ // A narrowed connection with no resolver wired loads nothing.
175
+ const unwired = await mount({ manuals: [GIT_TOOL], agentPath: agent.path });
176
+ expect((await fetch(`${unwired.base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(404);
177
+ });
178
+
136
179
  it('403s an unauthenticated caller', async () => {
137
180
  const { base } = await mount({ userId: '', manuals: [GIT_TOOL] });
138
181
  expect((await fetch(`${base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(403);