@bevel-software/platform-core-backend 0.22.0 → 0.24.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 (466) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +46 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +12 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +137 -24
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +46 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +99 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +16 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +28 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +6 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +17 -16
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  34. package/dist/modules/access/directory-sync-bot.js +7 -3
  35. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  36. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
  37. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  38. package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
  39. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  40. package/dist/modules/agent-instructions/compose.d.ts +38 -6
  41. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  42. package/dist/modules/agent-instructions/compose.js +39 -6
  43. package/dist/modules/agent-instructions/compose.js.map +1 -1
  44. package/dist/modules/agent-instructions/index.d.ts +2 -1
  45. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  46. package/dist/modules/agent-instructions/index.js +2 -1
  47. package/dist/modules/agent-instructions/index.js.map +1 -1
  48. package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
  49. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
  50. package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
  51. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
  52. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  53. package/dist/modules/audit/agent-audit.service.js +4 -2
  54. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  55. package/dist/modules/auth/account-admission.d.ts +58 -7
  56. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  57. package/dist/modules/auth/account-admission.js +44 -1
  58. package/dist/modules/auth/account-admission.js.map +1 -1
  59. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  60. package/dist/modules/auth/account-erasure.service.js +57 -16
  61. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  62. package/dist/modules/auth/account.routes.d.ts +1 -1
  63. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  64. package/dist/modules/auth/account.routes.js +61 -5
  65. package/dist/modules/auth/account.routes.js.map +1 -1
  66. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  67. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  68. package/dist/modules/auth/auth.middleware.js +18 -7
  69. package/dist/modules/auth/auth.middleware.js.map +1 -1
  70. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  71. package/dist/modules/auth/auth.routes.js +8 -0
  72. package/dist/modules/auth/auth.routes.js.map +1 -1
  73. package/dist/modules/auth/auth.service.d.ts +70 -2
  74. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  75. package/dist/modules/auth/auth.service.js +197 -18
  76. package/dist/modules/auth/auth.service.js.map +1 -1
  77. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  78. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  79. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  80. package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
  81. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  82. package/dist/modules/code-mode/code-mode.tool.js +66 -35
  83. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  84. package/dist/modules/database/connection.d.ts +16 -0
  85. package/dist/modules/database/connection.d.ts.map +1 -1
  86. package/dist/modules/database/connection.js +117 -0
  87. package/dist/modules/database/connection.js.map +1 -1
  88. package/dist/modules/database/core-schema.d.ts +337 -120
  89. package/dist/modules/database/core-schema.d.ts.map +1 -1
  90. package/dist/modules/database/core-schema.js +116 -58
  91. package/dist/modules/database/core-schema.js.map +1 -1
  92. package/dist/modules/database/migrate.d.ts +8 -0
  93. package/dist/modules/database/migrate.d.ts.map +1 -1
  94. package/dist/modules/database/migrate.js +271 -1
  95. package/dist/modules/database/migrate.js.map +1 -1
  96. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  97. package/dist/modules/kb-fs/branch-name.js +12 -2
  98. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  99. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  100. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  101. package/dist/modules/kb-fs/repo-path.js +79 -0
  102. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  104. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  105. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  106. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  108. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js +1 -1
  110. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  112. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  113. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  114. package/dist/modules/mcp/mcp.service.d.ts +8 -0
  115. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  116. package/dist/modules/mcp/mcp.service.js +38 -8
  117. package/dist/modules/mcp/mcp.service.js.map +1 -1
  118. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  119. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  120. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  121. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-request-records.store.js +7 -4
  123. package/dist/modules/plugins/join-request-records.store.js.map +1 -1
  124. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  125. package/dist/modules/settings/deployment-settings.service.js +8 -3
  126. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  127. package/dist/modules/settings/setup.routes.d.ts +52 -1
  128. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  129. package/dist/modules/settings/setup.routes.js +207 -19
  130. package/dist/modules/settings/setup.routes.js.map +1 -1
  131. package/dist/modules/skills/skills.contract.d.ts +90 -18
  132. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  133. package/dist/modules/skills/skills.contract.js +4 -1
  134. package/dist/modules/skills/skills.contract.js.map +1 -1
  135. package/dist/modules/skills/skills.service.d.ts +98 -9
  136. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  137. package/dist/modules/skills/skills.service.js +239 -37
  138. package/dist/modules/skills/skills.service.js.map +1 -1
  139. package/dist/modules/skills/skills.tools.d.ts +7 -0
  140. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  141. package/dist/modules/skills/skills.tools.js +71 -7
  142. package/dist/modules/skills/skills.tools.js.map +1 -1
  143. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  144. package/dist/modules/tool-auth/external-api-key.service.js +12 -4
  145. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  146. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  147. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  148. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  149. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  150. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  151. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  152. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  153. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  154. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  155. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  156. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  157. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  158. package/dist/modules/tool-helpers/validate-token.js +1 -1
  159. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  160. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  161. package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
  162. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  163. package/dist/modules/tool-registry/description-length.d.ts +80 -0
  164. package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
  165. package/dist/modules/tool-registry/description-length.js +108 -0
  166. package/dist/modules/tool-registry/description-length.js.map +1 -0
  167. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  168. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  169. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  170. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  171. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  172. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  173. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  174. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  175. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  176. package/dist/modules/workflow/file-lock.service.js +30 -0
  177. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  178. package/dist/modules/workflow/git/git.service.d.ts +94 -0
  179. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  180. package/dist/modules/workflow/git/git.service.js +193 -3
  181. package/dist/modules/workflow/git/git.service.js.map +1 -1
  182. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  183. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  184. package/dist/modules/workflow/pending-commits.service.js +60 -1
  185. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  186. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  187. package/dist/modules/workflow/recovery-bot.js +7 -3
  188. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  189. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  190. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  191. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  192. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  193. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  194. package/dist/modules/workflow/workflow-hooks.js +16 -1
  195. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  196. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  197. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  198. package/dist/modules/workflow/workflow.service.js +115 -5
  199. package/dist/modules/workflow/workflow.service.js.map +1 -1
  200. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  201. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  202. package/dist/modules/workspace/agent-access.gate.js +123 -0
  203. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  204. package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
  205. package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
  206. package/dist/modules/workspace/agent-upload.routes.js +210 -0
  207. package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
  208. package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
  209. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
  210. package/dist/modules/workspace/agent-upload.store.js +553 -0
  211. package/dist/modules/workspace/agent-upload.store.js.map +1 -0
  212. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  213. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  214. package/dist/modules/workspace/routine-write-policy.js +5 -6
  215. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  216. package/dist/modules/workspace/session-sink.d.ts +5 -5
  217. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  218. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  219. package/dist/modules/workspace/set-aside-clone.js +92 -0
  220. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  221. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  222. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  223. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  224. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  225. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  226. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
  227. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  228. package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
  229. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-source.js +46 -4
  231. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  232. package/dist/modules/workspace/upload-limits.d.ts +13 -0
  233. package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
  234. package/dist/modules/workspace/upload-limits.js +13 -0
  235. package/dist/modules/workspace/upload-limits.js.map +1 -0
  236. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  237. package/dist/modules/workspace/workspace.routes.js +93 -4
  238. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  239. package/dist/modules/workspace/workspace.service.d.ts +128 -5
  240. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  241. package/dist/modules/workspace/workspace.service.js +316 -58
  242. package/dist/modules/workspace/workspace.service.js.map +1 -1
  243. package/dist/modules/workspace/workspace.tools.d.ts +13 -11
  244. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  245. package/dist/modules/workspace/workspace.tools.js +791 -198
  246. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  247. package/dist/modules/workspace/write-denial.d.ts +0 -6
  248. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  249. package/dist/modules/workspace/write-denial.js +0 -6
  250. package/dist/modules/workspace/write-denial.js.map +1 -1
  251. package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
  252. package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
  253. package/dist/modules/workspace/zip-entry-rules.js +154 -0
  254. package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
  255. package/dist/modules/write-access/write-access.d.ts +60 -0
  256. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  257. package/dist/modules/write-access/write-access.js +129 -0
  258. package/dist/modules/write-access/write-access.js.map +1 -0
  259. package/dist/shared/column-crypto.d.ts +194 -0
  260. package/dist/shared/column-crypto.d.ts.map +1 -0
  261. package/dist/shared/column-crypto.js +144 -0
  262. package/dist/shared/column-crypto.js.map +1 -0
  263. package/dist/shared/domain-errors.d.ts +25 -0
  264. package/dist/shared/domain-errors.d.ts.map +1 -1
  265. package/dist/shared/domain-errors.js +28 -0
  266. package/dist/shared/domain-errors.js.map +1 -1
  267. package/dist/shared/git.contract.d.ts +20 -0
  268. package/dist/shared/git.contract.d.ts.map +1 -1
  269. package/dist/shared/git.contract.js +26 -0
  270. package/dist/shared/git.contract.js.map +1 -1
  271. package/dist/shared/token-crypto.d.ts.map +1 -1
  272. package/dist/shared/token-crypto.js +25 -1
  273. package/dist/shared/token-crypto.js.map +1 -1
  274. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  275. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  276. package/dist/tenancy/static-tenant-source.js +1 -2
  277. package/dist/tenancy/static-tenant-source.js.map +1 -1
  278. package/dist/tenancy/tenant-secrets.d.ts +5 -1
  279. package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
  280. package/dist/tenancy/tenant-secrets.js +4 -0
  281. package/dist/tenancy/tenant-secrets.js.map +1 -1
  282. package/kb-template/AGENTS.md +2 -0
  283. package/migrations/0014_change_request_closed_reason.sql +1 -0
  284. package/migrations/0015_account_deactivation.sql +3 -0
  285. package/migrations/0016_pii_encryption.sql +20 -0
  286. package/migrations/meta/0014_snapshot.json +2265 -0
  287. package/migrations/meta/0015_snapshot.json +2271 -0
  288. package/migrations/meta/0016_snapshot.json +2327 -0
  289. package/migrations/meta/_journal.json +21 -0
  290. package/package.json +3 -3
  291. package/src/__tests__/kb-layout-config.test.ts +0 -2
  292. package/src/__tests__/retired-settings.test.ts +96 -0
  293. package/src/core/__tests__/gated-boot.test.ts +132 -0
  294. package/src/core/__tests__/lifecycle.test.ts +241 -5
  295. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  296. package/src/core/core-ports.ts +11 -1
  297. package/src/core/create-core-server.ts +52 -16
  298. package/src/core/create-core-services.ts +156 -24
  299. package/src/core/lifecycle.ts +117 -13
  300. package/src/core-config.ts +31 -14
  301. package/src/index.ts +30 -1
  302. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  303. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  304. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  305. package/src/modules/access/__tests__/users-db-double.ts +21 -12
  306. package/src/modules/access/access-control.interface.ts +43 -12
  307. package/src/modules/access/access-control.service.ts +234 -36
  308. package/src/modules/access/access.routes.ts +17 -16
  309. package/src/modules/access/directory-sync-bot.ts +7 -3
  310. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
  311. package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
  312. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
  313. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
  314. package/src/modules/agent-instructions/compose.ts +50 -7
  315. package/src/modules/agent-instructions/index.ts +12 -0
  316. package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
  317. package/src/modules/audit/agent-audit.service.ts +4 -2
  318. package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
  319. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
  320. package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
  321. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  322. package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
  323. package/src/modules/auth/account-admission.ts +78 -9
  324. package/src/modules/auth/account-erasure.service.ts +69 -18
  325. package/src/modules/auth/account.routes.ts +63 -7
  326. package/src/modules/auth/auth.middleware.ts +19 -8
  327. package/src/modules/auth/auth.routes.ts +8 -0
  328. package/src/modules/auth/auth.service.ts +206 -23
  329. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  330. package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
  331. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
  332. package/src/modules/code-mode/code-mode.tool.ts +80 -34
  333. package/src/modules/database/__tests__/connection.test.ts +12 -0
  334. package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
  335. package/src/modules/database/connection.ts +117 -0
  336. package/src/modules/database/core-schema.ts +116 -58
  337. package/src/modules/database/migrate.ts +353 -1
  338. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  339. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  340. package/src/modules/kb-fs/branch-name.ts +14 -1
  341. package/src/modules/kb-fs/repo-path.ts +85 -0
  342. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  343. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  344. package/src/modules/kb-sync/sync-auth.ts +2 -2
  345. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  346. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
  347. package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
  348. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  349. package/src/modules/mcp/mcp.service.ts +46 -7
  350. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  351. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  352. package/src/modules/plugins/join-request-records.store.ts +7 -4
  353. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  354. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  355. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  356. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  357. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  358. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  359. package/src/modules/settings/deployment-settings.service.ts +8 -3
  360. package/src/modules/settings/setup.routes.ts +263 -19
  361. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  362. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  363. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  364. package/src/modules/skills/skills.contract.ts +91 -18
  365. package/src/modules/skills/skills.service.ts +278 -41
  366. package/src/modules/skills/skills.tools.ts +80 -8
  367. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  368. package/src/modules/tool-auth/external-api-key.service.ts +12 -4
  369. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  370. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  371. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  372. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  373. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  374. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  375. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  376. package/src/modules/tool-helpers/validate-token.ts +1 -1
  377. package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
  378. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
  379. package/src/modules/tool-registry/description-length.ts +111 -0
  380. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  381. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  382. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  383. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  384. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  385. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  386. package/src/modules/workflow/file-lock.service.ts +31 -0
  387. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  388. package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
  389. package/src/modules/workflow/git/git.service.ts +215 -1
  390. package/src/modules/workflow/pending-commits.service.ts +64 -2
  391. package/src/modules/workflow/recovery-bot.ts +7 -3
  392. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
  393. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
  394. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  395. package/src/modules/workflow/workflow-hooks.ts +64 -26
  396. package/src/modules/workflow/workflow.service.ts +125 -5
  397. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  398. package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
  399. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -0
  400. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  401. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  402. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  403. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  404. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  405. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
  406. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  407. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +402 -0
  408. package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
  409. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
  410. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  411. package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
  412. package/src/modules/workspace/agent-access.gate.ts +164 -0
  413. package/src/modules/workspace/agent-upload.routes.ts +214 -0
  414. package/src/modules/workspace/agent-upload.store.ts +668 -0
  415. package/src/modules/workspace/routine-write-policy.ts +5 -6
  416. package/src/modules/workspace/session-sink.ts +5 -5
  417. package/src/modules/workspace/set-aside-clone.ts +96 -0
  418. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  419. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  420. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
  421. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
  422. package/src/modules/workspace/startup/steps/template-source.ts +53 -5
  423. package/src/modules/workspace/upload-limits.ts +12 -0
  424. package/src/modules/workspace/workspace.routes.ts +96 -5
  425. package/src/modules/workspace/workspace.service.ts +319 -61
  426. package/src/modules/workspace/workspace.tools.ts +894 -214
  427. package/src/modules/workspace/write-denial.ts +0 -8
  428. package/src/modules/workspace/zip-entry-rules.ts +173 -0
  429. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  430. package/src/modules/write-access/write-access.ts +153 -0
  431. package/src/shared/__tests__/column-crypto.test.ts +217 -0
  432. package/src/shared/column-crypto.ts +218 -0
  433. package/src/shared/domain-errors.ts +31 -0
  434. package/src/shared/git.contract.ts +27 -0
  435. package/src/shared/token-crypto.ts +28 -1
  436. package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
  437. package/src/tenancy/static-tenant-source.ts +1 -3
  438. package/src/tenancy/tenant-secrets.ts +5 -1
  439. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  440. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  441. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  442. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  443. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  444. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  445. package/dist/modules/workflow/session-ontology.service.js +0 -147
  446. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  447. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  448. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  449. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  450. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  451. package/dist/shared/kb-layout.d.ts +0 -39
  452. package/dist/shared/kb-layout.d.ts.map +0 -1
  453. package/dist/shared/kb-layout.js +0 -103
  454. package/dist/shared/kb-layout.js.map +0 -1
  455. package/dist/shared/kb-layout.test.d.ts +0 -2
  456. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  457. package/dist/shared/kb-layout.test.js +0 -75
  458. package/dist/shared/kb-layout.test.js.map +0 -1
  459. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  460. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  461. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  462. package/src/modules/workflow/session-ontology.service.ts +0 -183
  463. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  464. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  465. package/src/shared/kb-layout.test.ts +0 -98
  466. package/src/shared/kb-layout.ts +0 -102
@@ -0,0 +1,385 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+
6
+ import { NodeFs } from '../../kb-fs/node-fs.js';
7
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
8
+ import { AccessControlService } from '../access-control.service.js';
9
+
10
+ /**
11
+ * `previewAccessAfterRelocation`: the caller's OWN verbs at a destination as
12
+ * they will be once a move or a copy has landed there.
13
+ *
14
+ * The question the four gates cannot answer, because the `access.md` files a
15
+ * folder carries are not at the destination yet — which is how a rename came
16
+ * to warn about losing owner access the folder's own rules were about to hand
17
+ * straight back. Driven over a real on-disk tree, and every answer checked
18
+ * against the ordinary gates once the tree really says what the preview said
19
+ * it would.
20
+ */
21
+
22
+ const KB_DIR = 'knowledge-base';
23
+ const MOVER = 'mover@x.io';
24
+
25
+ function stubWorkspaceService(workspaceId: string, workspaceDir: string): WorkspaceService {
26
+ return {
27
+ getWorkspacePath: async (id: string) => {
28
+ if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
29
+ return workspaceDir;
30
+ },
31
+ ensureRemotesFetched: async () => undefined,
32
+ } as unknown as WorkspaceService;
33
+ }
34
+
35
+ const rules = (body: string) => `---\n${body}---\n`;
36
+
37
+ describe('AccessControlService.previewAccessAfterRelocation', () => {
38
+ let root: string;
39
+ let repo: string;
40
+ const workspaceId = 'ws-preview-relocation';
41
+
42
+ beforeEach(async () => {
43
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-preview-reloc-'));
44
+ repo = path.join(root, workspaceId, KB_DIR);
45
+ });
46
+ afterEach(async () => {
47
+ if (root) await fs.rm(root, { recursive: true, force: true });
48
+ });
49
+
50
+ async function write(files: Record<string, string>): Promise<void> {
51
+ for (const [rel, contents] of Object.entries(files)) {
52
+ const abs = path.join(repo, rel);
53
+ await fs.mkdir(path.dirname(abs), { recursive: true });
54
+ await fs.writeFile(abs, contents);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * A service reading the tree as it is NOW. Built fresh after every change
60
+ * on purpose: in production the file-change notifier invalidates the model,
61
+ * and a test that leaned on the five-second cache would be asserting the
62
+ * cache rather than the resolver.
63
+ */
64
+ const service = () =>
65
+ new AccessControlService(
66
+ stubWorkspaceService(workspaceId, path.join(root, workspaceId)),
67
+ KB_DIR,
68
+ new NodeFs(),
69
+ );
70
+
71
+ /** The caller's four verbs at a path, through the ordinary gates. */
72
+ async function verbsAt(svc: AccessControlService, rel: string) {
73
+ const [read, write, download, owner] = await Promise.all([
74
+ svc.canRead(workspaceId, MOVER, rel),
75
+ svc.canWrite(workspaceId, MOVER, rel),
76
+ svc.canDownload(workspaceId, MOVER, rel),
77
+ svc.canOwner(workspaceId, MOVER, rel),
78
+ ]);
79
+ return { read, write, download, owner };
80
+ }
81
+
82
+ /** Move `from` to `to` on disk, as the move itself does. */
83
+ async function reallyMove(from: string, to: string): Promise<void> {
84
+ const dest = path.join(repo, to);
85
+ await fs.mkdir(path.dirname(dest), { recursive: true });
86
+ await fs.rename(path.join(repo, from), dest);
87
+ }
88
+
89
+ /** Copy `from` to `to` on disk, leaving the source where it is. */
90
+ async function reallyCopy(from: string, to: string): Promise<void> {
91
+ const dest = path.join(repo, to);
92
+ await fs.mkdir(path.dirname(dest), { recursive: true });
93
+ await fs.cp(path.join(repo, from), dest, { recursive: true });
94
+ }
95
+
96
+ const BASE = {
97
+ 'roles.yaml': 'roles:\n Admin:\n - admin@x.io\n',
98
+ 'access.md': rules('read:\n - everyone\n'),
99
+ };
100
+
101
+ it('a folder keeps the access its own access.md gives it when it is renamed', async () => {
102
+ await write({
103
+ ...BASE,
104
+ 'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
105
+ 'Sales/deal.md': '# Deal\n',
106
+ });
107
+
108
+ const svc = service();
109
+ const before = await verbsAt(svc, 'Sales');
110
+ const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Sales', 'Revenue');
111
+
112
+ expect(before.owner).toBe(true);
113
+ expect(after).toEqual(before);
114
+
115
+ // The root grants read and nothing else, so the destination AS IT STANDS
116
+ // says the opposite — which is the warning people were shown.
117
+ expect(await verbsAt(svc, 'Revenue')).toMatchObject({ write: false, owner: false });
118
+
119
+ await reallyMove('Sales', 'Revenue');
120
+ expect(await verbsAt(service(), 'Revenue')).toEqual(after);
121
+ });
122
+
123
+ it('a folder with no access.md of its own takes the new parent\'s rules, as it always did', async () => {
124
+ await write({
125
+ ...BASE,
126
+ 'Work/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
127
+ 'Work/Notes/note.md': '# Note\n',
128
+ 'Legal/access.md': rules('read:\n - everyone\n'),
129
+ });
130
+
131
+ const svc = service();
132
+ const before = await verbsAt(svc, 'Work/Notes');
133
+ const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Notes', 'Legal/Notes');
134
+
135
+ expect(before.write).toBe(true);
136
+ expect(after.write).toBe(false);
137
+ // Nothing travels, so the preview is exactly what the gates say today.
138
+ expect(after).toEqual(await verbsAt(svc, 'Legal/Notes'));
139
+
140
+ await reallyMove('Work/Notes', 'Legal/Notes');
141
+ expect(await verbsAt(service(), 'Legal/Notes')).toEqual(after);
142
+ });
143
+
144
+ it('what the folder inherited is left behind; what it carries comes along', async () => {
145
+ await write({
146
+ ...BASE,
147
+ 'Work/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
148
+ 'Work/Team/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
149
+ 'Work/Team/plan.md': '# Plan\n',
150
+ 'Legal/access.md': rules('read:\n - everyone\n'),
151
+ });
152
+
153
+ const svc = service();
154
+ expect(await verbsAt(svc, 'Work/Team')).toMatchObject({ write: true, owner: true });
155
+
156
+ const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team', 'Legal/Team');
157
+
158
+ // Write is the folder's own and travels; owner came from `Work/` and
159
+ // does not.
160
+ expect(after).toMatchObject({ write: true, owner: false });
161
+
162
+ await reallyMove('Work/Team', 'Legal/Team');
163
+ expect(await verbsAt(service(), 'Legal/Team')).toEqual(after);
164
+ });
165
+
166
+ it('a nested access.md does not change the answer at the folder itself', async () => {
167
+ const tree = {
168
+ ...BASE,
169
+ 'Work/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
170
+ 'Work/Team/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
171
+ 'Work/Team/plan.md': '# Plan\n',
172
+ 'Legal/access.md': rules('read:\n - everyone\n'),
173
+ };
174
+ await write(tree);
175
+ const withoutNested = await service().previewAccessAfterRelocation(
176
+ workspaceId, MOVER, 'Work/Team', 'Legal/Team',
177
+ );
178
+
179
+ // `Sub/` shuts the caller out entirely, and it moves too — but it governs
180
+ // `Legal/Team/Sub`, not `Legal/Team`.
181
+ await write({ 'Work/Team/Sub/access.md': rules(`read:\n - deny Mover <${MOVER}>\n`) });
182
+ const svc = service();
183
+ expect(await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team', 'Legal/Team'))
184
+ .toEqual(withoutNested);
185
+ // It is counted, though — at the path it lands on.
186
+ expect(
187
+ await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team', 'Legal/Team'),
188
+ ).not.toEqual(
189
+ await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/Team/Sub', 'Legal/Team/Sub'),
190
+ );
191
+
192
+ await reallyMove('Work/Team', 'Legal/Team');
193
+ const after = service();
194
+ expect(await verbsAt(after, 'Legal/Team')).toEqual(withoutNested);
195
+ expect(await verbsAt(after, 'Legal/Team/Sub')).toMatchObject({ read: false });
196
+ });
197
+
198
+ it('a copy leaves the source rules where they are and lands a second set', async () => {
199
+ await write({
200
+ ...BASE,
201
+ 'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
202
+ 'Sales/deal.md': '# Deal\n',
203
+ // A destination governed DIFFERENTLY from the source, so the answer can
204
+ // only come from the rules the copy carries: Legal grants read and
205
+ // nothing else, as the root does.
206
+ 'Legal/access.md': rules('read:\n - everyone\n'),
207
+ });
208
+
209
+ const svc = service();
210
+ const after = await svc.previewAccessAfterRelocation(
211
+ workspaceId, MOVER, 'Sales', 'Legal/Sales-Copy', { sourceRemains: true },
212
+ );
213
+
214
+ // The copied `access.md` governs the copy, which is the opposite of what
215
+ // the destination says today.
216
+ expect(after).toMatchObject({ write: true, owner: true });
217
+ expect(await verbsAt(svc, 'Legal')).toMatchObject({ write: false, owner: false });
218
+
219
+ await reallyCopy('Sales', 'Legal/Sales-Copy');
220
+ const done = service();
221
+ expect(await verbsAt(done, 'Legal/Sales-Copy')).toEqual(after);
222
+ // `sourceRemains`: both sets of rules are real afterwards, and the source
223
+ // keeps the access it always had.
224
+ expect(await verbsAt(done, 'Sales')).toMatchObject({ owner: true });
225
+ });
226
+
227
+ /**
228
+ * A lone `access.md` is a single FILE whose rules are a whole folder's.
229
+ * Keyed by the directory it sits in, it is not a path "under" the source,
230
+ * so the folder walk cannot see it — and `copy_file` will copy one, which
231
+ * is how a preview came to answer the destination's old rules about a
232
+ * directory that was about to be governed by the file landing in it.
233
+ */
234
+ it('a lone access.md governs the folder it is copied into', async () => {
235
+ await write({
236
+ ...BASE,
237
+ 'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
238
+ 'Legal/note.md': '# Note\n',
239
+ });
240
+
241
+ const svc = service();
242
+ // `Legal/` has no rules of its own; the root grants read and nothing else.
243
+ expect(await verbsAt(svc, 'Legal')).toMatchObject({ write: false, owner: false });
244
+
245
+ const after = await svc.previewAccessAfterRelocation(
246
+ workspaceId, MOVER, 'Sales/access.md', 'Legal/access.md', { sourceRemains: true },
247
+ );
248
+ expect(after).toMatchObject({ write: true, owner: true });
249
+
250
+ await fs.copyFile(path.join(repo, 'Sales/access.md'), path.join(repo, 'Legal/access.md'));
251
+ const done = service();
252
+ expect(await verbsAt(done, 'Legal/access.md')).toEqual(after);
253
+ // What landed governs the folder, not only itself.
254
+ expect(await verbsAt(done, 'Legal')).toMatchObject({ write: true, owner: true });
255
+ });
256
+
257
+ /**
258
+ * The name the bytes land under decides what they GOVERN, and it decides
259
+ * how they are PARSED: the same frontmatter is a folder's rules in an
260
+ * `access.md` and the file's own rules anywhere else. So these bytes
261
+ * govern no folder at `Legal/rules-copy.md` — and still grant the caller
262
+ * write and owner, on that one file. Reading them as the source's name said
263
+ * `write: false, owner: false`, and the copy then handed over both: a
264
+ * preview contradicting the operation it was previewing, which is the
265
+ * entire fault this ticket exists to kill.
266
+ */
267
+ it('a file copied under any other name governs no folder, but keeps its own grants', async () => {
268
+ await write({
269
+ ...BASE,
270
+ 'Sales/access.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
271
+ 'Legal/note.md': '# Note\n',
272
+ });
273
+
274
+ const svc = service();
275
+ const after = await svc.previewAccessAfterRelocation(
276
+ workspaceId, MOVER, 'Sales/access.md', 'Legal/rules-copy.md', { sourceRemains: true },
277
+ );
278
+ // Pinned, not merely equal to the outcome: both sides reading `false`
279
+ // would satisfy the comparison below and be wrong together.
280
+ expect(after).toMatchObject({ write: true, owner: true });
281
+
282
+ await reallyCopy('Sales/access.md', 'Legal/rules-copy.md');
283
+ const done = service();
284
+ expect(await verbsAt(done, 'Legal/rules-copy.md')).toEqual(after);
285
+ // The folder it landed in is untouched: an ordinary note governs nothing
286
+ // but itself, whatever its frontmatter says.
287
+ expect(await verbsAt(done, 'Legal')).toMatchObject({ write: false, owner: false });
288
+ expect(await verbsAt(done, 'Legal/note.md')).toMatchObject({ write: false, owner: false });
289
+ });
290
+
291
+ /**
292
+ * And the mirror, which `copy_file` also allows: ordinary bytes landing as
293
+ * a directory's `access.md` become that directory's rules the moment they
294
+ * land. The preview reads them as the destination will.
295
+ */
296
+ it('a file copied TO an access.md gives the destination folder its rules', async () => {
297
+ await write({
298
+ ...BASE,
299
+ 'Draft/proposed-rules.md': rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
300
+ 'Legal/note.md': '# Note\n',
301
+ });
302
+
303
+ const svc = service();
304
+ expect(await verbsAt(svc, 'Legal')).toMatchObject({ write: false, owner: false });
305
+
306
+ const after = await svc.previewAccessAfterRelocation(
307
+ workspaceId, MOVER, 'Draft/proposed-rules.md', 'Legal/access.md', { sourceRemains: true },
308
+ );
309
+ expect(after).toMatchObject({ write: true, owner: true });
310
+
311
+ await reallyCopy('Draft/proposed-rules.md', 'Legal/access.md');
312
+ const done = service();
313
+ expect(await verbsAt(done, 'Legal/access.md')).toEqual(after);
314
+ // It governs the folder now, which is the whole difference the name makes.
315
+ expect(await verbsAt(done, 'Legal')).toMatchObject({ write: true, owner: true });
316
+ expect(await verbsAt(done, 'Legal/note.md')).toMatchObject({ write: true, owner: true });
317
+ });
318
+
319
+ /**
320
+ * The same rule for the other thing that travels with a file: its own
321
+ * frontmatter. Asking the DESTINATION for it always answered null — nothing
322
+ * is there yet — so a rename of a self-governing file previewed a loss the
323
+ * move hands straight back, and the preview contradicted the access the
324
+ * caller really has afterwards (Specification requirement 6).
325
+ */
326
+ it("a file's own frontmatter travels with its bytes, so a rename costs nothing", async () => {
327
+ await write({
328
+ ...BASE,
329
+ 'Work/access.md': rules('read:\n - everyone\n'),
330
+ 'Work/plan.md': rules(`owner:\n - Mover <${MOVER}>\n`) + '# Plan\n',
331
+ });
332
+
333
+ const svc = service();
334
+ const before = await verbsAt(svc, 'Work/plan.md');
335
+ expect(before.owner).toBe(true);
336
+
337
+ const after = await svc.previewAccessAfterRelocation(
338
+ workspaceId, MOVER, 'Work/plan.md', 'Work/roadmap.md',
339
+ );
340
+ expect(after).toEqual(before);
341
+
342
+ await reallyMove('Work/plan.md', 'Work/roadmap.md');
343
+ expect(await verbsAt(service(), 'Work/roadmap.md')).toEqual(after);
344
+ });
345
+
346
+ it('a single file answers exactly as the gates at the destination do', async () => {
347
+ await write({
348
+ ...BASE,
349
+ 'Work/access.md': rules(`write:\n - Mover <${MOVER}>\n`),
350
+ 'Work/note.md': '# Note\n',
351
+ 'Legal/access.md': rules('read:\n - everyone\n'),
352
+ });
353
+
354
+ const svc = service();
355
+ const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Work/note.md', 'Legal/note.md');
356
+
357
+ expect(after).toEqual(await verbsAt(svc, 'Legal/note.md'));
358
+
359
+ await reallyMove('Work/note.md', 'Legal/note.md');
360
+ expect(await verbsAt(service(), 'Legal/note.md')).toEqual(after);
361
+ });
362
+
363
+ it('answers the caller\'s own verbs and nothing else, and writes nothing', async () => {
364
+ await write({
365
+ ...BASE,
366
+ 'Sales/access.md': rules(`read:\n - deny everyone\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`),
367
+ 'Sales/deal.md': '# Deal\n',
368
+ });
369
+
370
+ const svc = service();
371
+ const after = await svc.previewAccessAfterRelocation(workspaceId, MOVER, 'Sales', 'Revenue');
372
+
373
+ // Four booleans. Nothing about who else holds what, and nothing out of
374
+ // an `access.md` the caller may not read.
375
+ expect(Object.keys(after).sort()).toEqual(['download', 'owner', 'read', 'write']);
376
+ for (const v of Object.values(after)) expect(typeof v).toBe('boolean');
377
+
378
+ // A stranger gets their own answer, not the caller's.
379
+ expect(await svc.previewAccessAfterRelocation(workspaceId, 'stranger@x.io', 'Sales', 'Revenue'))
380
+ .toMatchObject({ read: false, owner: false });
381
+
382
+ expect(await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8')).toContain('owner:');
383
+ await expect(fs.access(path.join(repo, 'Revenue'))).rejects.toThrow();
384
+ });
385
+ });
@@ -9,11 +9,12 @@ import { AccessControlService } from '../access-control.service.js';
9
9
  import { holderPrincipals } from '../access-view.js';
10
10
 
11
11
  /**
12
- * `prospectiveHolders`: who can open and edit a file where it sits, and who
13
- * could once a move puts it somewhere else. Driven over a real on-disk tree
14
- * through the public resolver, because the whole point is that the "after"
15
- * side must be the SAME resolution as a real one — the destination's folder
16
- * chain with the file's own frontmatter on top — for a path that does not
12
+ * `prospectiveHolders`: who can open and edit a file or folder where it
13
+ * sits, and who could once a move puts it somewhere else. Driven over a real
14
+ * on-disk tree through the public resolver, because the whole point is that
15
+ * the "after" side must be the SAME resolution as a real one — the
16
+ * destination's folder chain with whatever travels laid over it, a file's own
17
+ * frontmatter or a folder's `access.md` files — for a path that does not
17
18
  * exist yet and therefore cannot be asked for in the ordinary way.
18
19
  */
19
20
 
@@ -136,25 +137,100 @@ describe('AccessControlService.prospectiveHolders', () => {
136
137
  expect(holderPrincipals(before.read)).toContainEqual({ kind: 'group', name: 'Engineering' });
137
138
  });
138
139
 
139
- it('refuses a folder source rather than resolving it as a file', async () => {
140
+ /**
141
+ * A folder carries its own `access.md`, and that file MOVES WITH IT. The
142
+ * resolver used to refuse the question rather than answer it as if the
143
+ * folder were a file — the right refusal for the wrong reason, since the
144
+ * answer it could not give is the one a folder move actually needs.
145
+ */
146
+ it('answers a folder source with the rules it carries, at the path they land on', async () => {
140
147
  const svc = await makeService(TREE);
141
148
 
142
- // `Knowledge/Legal` carries its own access.md and governs everything under
143
- // it. Resolved as a file it would report the rules of the folder ABOVE it
144
- // and none of its own — a confident, wrong answer.
145
- await expect(
146
- svc.prospectiveHolders(workspaceId, 'Knowledge/Legal', 'Knowledge/Sales/Legal'),
147
- ).rejects.toThrow(/folder/i);
149
+ const { before, after } = await svc.prospectiveHolders(
150
+ workspaceId,
151
+ 'Knowledge/Legal',
152
+ 'Knowledge/Sales/Legal',
153
+ );
154
+
155
+ // `Knowledge/Legal/access.md` travels, so Engineering keeps both verbs at
156
+ // the new path — the rules the folder brings, not the ones it is landing
157
+ // among. Read as a FILE the answer would have been Product's, from
158
+ // `Knowledge/Sales/access.md` alone.
159
+ expect(names(before.read)).toContain('Engineering');
160
+ expect(names(before.write)).toContain('Engineering');
161
+ expect(names(after.read)).toContain('Engineering');
162
+ expect(names(after.write)).toContain('Engineering');
163
+ });
164
+
165
+ it('leaves the old parent behind and picks the new one up', async () => {
166
+ const svc = await makeService({
167
+ ...TREE,
168
+ // Engineering's write on Team comes from Legal above it, not from Team.
169
+ 'Knowledge/Legal/Team/access.md': rules('read:\n - Product\n'),
170
+ 'Knowledge/Legal/Team/plan.md': '# Plan\n',
171
+ });
172
+
173
+ const { before, after } = await svc.prospectiveHolders(
174
+ workspaceId,
175
+ 'Knowledge/Legal/Team',
176
+ 'Knowledge/Sales/Team',
177
+ );
178
+
179
+ expect(names(before.write)).toContain('Engineering');
180
+ // Sales grants nobody write, and Team's own file grants only read — so
181
+ // the write Team held by inheritance does not come along.
182
+ expect(names(after.write)).not.toContain('Engineering');
183
+ expect(names(after.read)).toContain('Product');
184
+ });
185
+
186
+ it('a rename of a folder that grants its own access changes nothing', async () => {
187
+ const svc = await makeService(TREE);
188
+
189
+ const { before, after } = await svc.prospectiveHolders(
190
+ workspaceId,
191
+ 'Knowledge/Legal',
192
+ 'Knowledge/Revenue',
193
+ );
194
+
195
+ expect(names(after.read)).toEqual(names(before.read));
196
+ expect(names(after.write)).toEqual(names(before.write));
197
+ });
198
+
199
+ /**
200
+ * A SYMLINK is neither of the two shapes the preview knows. `move_file`
201
+ * refuses a symlink source outright (`assertNoSymlinkOnPath`), so a link to
202
+ * a directory must not be described as a folder taking its `access.md`
203
+ * files along — that would promise the target's rules at a path nothing is
204
+ * ever going to arrive at. Nothing is read through the link either.
205
+ */
206
+ it('a symlink to a folder is not previewed as the folder it points at', async () => {
207
+ const svc = await makeService(TREE);
208
+ const repo = path.join(root, workspaceId, KB_DIR);
209
+ await fs.symlink(path.join(repo, 'Knowledge/Legal'), path.join(repo, 'Knowledge/Sales/alias'));
210
+
211
+ const { after } = await svc.prospectiveHolders(
212
+ workspaceId,
213
+ 'Knowledge/Sales/alias',
214
+ 'Knowledge/alias',
215
+ );
216
+
217
+ // Legal's own rules stayed behind the link: `Knowledge/` has no
218
+ // `access.md`, so nobody holds anything at the destination.
219
+ expect(names(after.read)).not.toContain('Engineering');
220
+ expect(names(after.write)).not.toContain('Engineering');
148
221
  });
149
222
 
150
223
  it('a probe that fails for any reason but absence throws, never a silent "file"', async () => {
151
224
  const svc = await makeService(TREE);
152
- // A disk fault (EACCES here) must never read as "not a directory": that is
153
- // exactly how a folder source would slip past the refusal above, and the
154
- // answer it slips into names the wrong principals. Spied rather than
155
- // staged with mode bits, so the property holds as root and on Windows too.
156
- const real = fs.stat;
157
- const spy = vi.spyOn(fs, 'stat').mockImplementation(((p: string) =>
225
+ // A disk fault (EACCES here) must never read as "not a directory": the
226
+ // probe is what chooses between the two resolutions, so a folder read as
227
+ // a file would be answered with frontmatter it does not have and without
228
+ // the `access.md` files it carries — the wrong principals, named with
229
+ // the same confidence as the right ones. Spied rather than staged with
230
+ // mode bits, so the property holds as root and on Windows too. `lstat`,
231
+ // because that is the probe: a symlink is classified by what it IS.
232
+ const real = fs.lstat;
233
+ const spy = vi.spyOn(fs, 'lstat').mockImplementation(((p: string) =>
158
234
  String(p).endsWith('contract.md')
159
235
  ? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
160
236
  : (real as (p: string) => Promise<unknown>).call(fs, p)) as never);
@@ -10,7 +10,6 @@ import type { WorkflowService } from '../../workflow/workflow.service.js';
10
10
  import type { WorkflowEventBus } from '../../workflow/event-bus.js';
11
11
  import type { Database } from '../../database/connection.js';
12
12
  import { createAccessRoutes } from '../access.routes.js';
13
- import { WorkflowDomainError } from '../../../shared/domain-errors.js';
14
13
  import { testKbContext } from '../../../__tests__/kb-context.js';
15
14
 
16
15
  /**
@@ -192,15 +191,15 @@ describe('GET /access/prospective', () => {
192
191
  expect(await res.json()).toEqual({ error: 'Internal error.' });
193
192
  });
194
193
 
195
- it('refuses a folder source the way the resolver does, without guessing', async () => {
194
+ it('asks about a folder source the same way, keeping its name at the destination', async () => {
196
195
  h = await makeHarness();
197
- h.prospectiveHolders.mockRejectedValueOnce(
198
- new WorkflowDomainError('prospective access answers for a file, not a folder', 400),
199
- );
200
196
 
201
- const res = await get(`from=${encodeURIComponent('Knowledge/Legal')}&toDir=Knowledge`);
197
+ const res = await get(`from=${encodeURIComponent('Knowledge/Legal')}&toDir=${encodeURIComponent('Knowledge/Sales')}`);
202
198
 
203
- expect(res.status).toBe(400);
199
+ expect(res.status).toBe(200);
200
+ // The route derives the destination from the name, folder or file alike;
201
+ // which of the two it is, and what travels with it, is the resolver's.
202
+ expect(h.prospectiveHolders).toHaveBeenCalledWith(WS, 'Knowledge/Legal', 'Knowledge/Sales/Legal');
204
203
  });
205
204
 
206
205
  it('falls back to the name-only roles list when the resolver omits kinds', async () => {
@@ -1,10 +1,11 @@
1
1
  import { Param } from 'drizzle-orm';
2
2
 
3
3
  import type { Database } from '../../database/connection.js';
4
+ import { users } from '../../database/schema.js';
4
5
 
5
6
  /**
6
- * Every literal bound into a drizzle condition, in order — for
7
- * `inArray(users.email, emails)` that is exactly `emails`.
7
+ * Every address a drizzle condition binds to the users' blind index, in
8
+ * order — for `inArray(users.emailBidx, emails)` that is exactly `emails`.
8
9
  *
9
10
  * A drizzle `SQL` is a tree of chunks; the bound values sit in it as `Param`
10
11
  * nodes (an `inArray` puts them in a nested array chunk). Reading them back is
@@ -14,7 +15,12 @@ import type { Database } from '../../database/connection.js';
14
15
  */
15
16
  function boundValues(node: unknown, depth = 0): string[] {
16
17
  if (depth > 8 || node === null || node === undefined) return [];
17
- if (node instanceof Param) return typeof node.value === 'string' ? [node.value] : [];
18
+ // Only what is bound THROUGH THE INDEX COLUMN counts: an address compared
19
+ // with the encrypted `email` column matches nothing in Postgres (the
20
+ // ciphertext is randomized), so it matches nothing here.
21
+ if (node instanceof Param) {
22
+ return node.encoder === users.emailBidx && typeof node.value === 'string' ? [node.value] : [];
23
+ }
18
24
  if (Array.isArray(node)) return node.flatMap((c) => boundValues(c, depth + 1));
19
25
  const chunks = (node as { queryChunks?: unknown }).queryChunks;
20
26
  return Array.isArray(chunks) ? chunks.flatMap((c) => boundValues(c, depth + 1)) : [];
@@ -30,14 +36,17 @@ function boundValues(node: unknown, depth = 0): string[] {
30
36
  * routes ask about comes back without one, which is what makes the share
31
37
  * dialog label it "hasn't signed in yet".
32
38
  *
33
- * The `where` clause IS evaluated: the emails bound into it are matched
34
- * against the roster the same way Postgres would — against the stored rows as
35
- * written, with no re-canonicalisation on the double's side. Real rows are
36
- * canonical (the auth service lowercases every address it writes), so a
37
- * caller that stopped lowering the emails it asks about would match nothing
38
- * here, exactly as it would match nothing in Postgres. A double that answered
39
- * with the whole roster could not tell those two apart.
39
+ * The `where` clause IS evaluated: the values bound into it are matched
40
+ * against the roster the same way Postgres would. The email column is
41
+ * randomized ciphertext, so the routes compare its blind index
42
+ * (`email_bidx`), which they bind the ADDRESSES to — the database handle
43
+ * turns each into its index, over the canonical (trimmed, lowercased)
44
+ * address. The double does what the index does: a caller that stopped
45
+ * lowering the emails it asks about still matches here, as it does in
46
+ * Postgres.
40
47
  */
48
+ const canonical = (email: string) => email.trim().toLowerCase();
49
+
41
50
  export function usersDbDouble(
42
51
  accounts: readonly (string | { email: string; name?: string })[] = [],
43
52
  ): Database {
@@ -54,8 +63,8 @@ export function usersDbDouble(
54
63
  where: (condition: unknown) => Promise<typeof rows>;
55
64
  };
56
65
  query.where = async (condition: unknown) => {
57
- const asked = new Set(boundValues(condition));
58
- return rows.filter((r) => asked.has(r.email));
66
+ const asked = new Set(boundValues(condition).map(canonical));
67
+ return rows.filter((r) => asked.has(canonical(r.email)));
59
68
  };
60
69
  return query;
61
70
  };