@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
@@ -84,11 +84,14 @@ export type HolderList = {
84
84
  export type PathHolders = { read: HolderList; write: HolderList };
85
85
 
86
86
  /**
87
- * One file's holders where it is now and where a move would put it — see
88
- * {@link IAccessControl.prospectiveHolders}.
87
+ * One file's or folder's holders where it is now and where a move would put
88
+ * it — see {@link IAccessControl.prospectiveHolders}.
89
89
  */
90
90
  export type ProspectiveHolders = { before: PathHolders; after: PathHolders };
91
91
 
92
+ /** One caller's own verdict per access verb on one path. */
93
+ export type PathVerbs = { read: boolean; write: boolean; download: boolean; owner: boolean };
94
+
92
95
  /**
93
96
  * Per-verb sources of a principal's access on a target. Only verbs the principal
94
97
  * actually holds (via a named file entry) appear; each maps to the closest-first
@@ -358,19 +361,17 @@ export interface IAccessControl {
358
361
  }>;
359
362
 
360
363
  /**
361
- * Who can open and who can edit one file where it IS, and where a move
362
- * would put it. `toPath` names a path that does not exist yet — the point
363
- * of the call is to answer before the move happens — so the resolution
364
- * layers the file's OWN rules (its frontmatter, read from `fromPath`,
365
- * which travels with the bytes) over the destination's folder chain.
364
+ * Who can open and who can edit one file or folder where it IS, and where a
365
+ * move would put it. `toPath` names a path that does not exist yet — the
366
+ * point of the call is to answer before the move happens — so the
367
+ * resolution is over what travels with the bytes: a file's OWN rules (its
368
+ * frontmatter, read from `fromPath`) layered over the destination's folder
369
+ * chain, or a folder's `access.md` files keyed at the directories they
370
+ * land in. Either way the rules the source inherited from its old parent
371
+ * are left behind and the new parent's apply.
366
372
  *
367
373
  * Writes nothing and moves nothing. The move confirmation diffs the two
368
374
  * sides to name who loses and who gains access.
369
- *
370
- * A FILE question only: a `fromPath` that is a directory is refused with a
371
- * 400. A folder's access is its own `access.md` — which moves with it and
372
- * governs everything beneath it — so resolving it as a file would name the
373
- * wrong principals with the same confidence as the right ones.
374
375
  */
375
376
  prospectiveHolders(
376
377
  workspaceId: string,
@@ -378,6 +379,36 @@ export interface IAccessControl {
378
379
  toPath: string,
379
380
  ): Promise<ProspectiveHolders>;
380
381
 
382
+ /**
383
+ * The CALLER'S OWN `{ read, write, download, owner }` at `toPath` as they
384
+ * will be once `fromPath` has been moved (`sourceRemains: false`, the
385
+ * default) or copied (`sourceRemains: true`) there — the question a move's
386
+ * or copy's dry run asks, which the same four gates at `toPath` cannot
387
+ * answer because what travels with the bytes is not at the destination yet:
388
+ * the `access.md` files inside a folder, keyed at the directories they land
389
+ * in, and a file's own frontmatter.
390
+ *
391
+ * What a single file's rules GOVERN follows the name it lands under, not the
392
+ * one it leaves: landing as a directory's `access.md` they are that
393
+ * directory's rules (so copying `Sales/access.md` to `Legal/access.md`
394
+ * previews the access `Legal/` is about to have), and landing under any
395
+ * other name they are the file's own and govern nothing but it. Either way
396
+ * the answer is the one the gates will give a moment later.
397
+ *
398
+ * PREVIEW ONLY. It describes a tree that does not exist, so it must never
399
+ * decide whether an operation may run: the gates keep resolving against the
400
+ * tree that does. It writes nothing and moves nothing, and it answers the
401
+ * caller's own verbs only — never the content of an `access.md` they may
402
+ * not read.
403
+ */
404
+ previewAccessAfterRelocation(
405
+ workspaceId: string,
406
+ userEmail: string,
407
+ fromPath: string,
408
+ toPath: string,
409
+ opts?: { sourceRemains?: boolean },
410
+ ): Promise<PathVerbs>;
411
+
381
412
  /**
382
413
  * Finite expanded email set for configured users who could approve this path
383
414
  * — role members + direct user grants, minus anyone denied. The built-in
@@ -5,7 +5,7 @@ import { logger } from '../../shared/logging.js';
5
5
  const log = logger('access');
6
6
 
7
7
  import { isAbsence, type ITreeWalker, type WalkListener } from '../../shared/fs.contract.js';
8
- import { isGitTimeout, type IGitRunner } from '../../shared/git.contract.js';
8
+ import { assertOneSpecPerLine, isGitTimeout, type IGitRunner } from '../../shared/git.contract.js';
9
9
  import { hashEmail } from '../../shared/email-identity.js';
10
10
  import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
11
11
  import type { WorkspaceService } from '../workspace/workspace.service.js';
@@ -19,6 +19,7 @@ import type {
19
19
  GrantSource,
20
20
  GrantSources,
21
21
  PathHolders,
22
+ PathVerbs,
22
23
  ProspectiveHolders,
23
24
  ResolvedPrincipal,
24
25
  } from './access-control.interface.js';
@@ -31,7 +32,6 @@ import {
31
32
  } from '@bevel-software/platform-shared';
32
33
  import type { KbContext } from '../../shared/kb-context.js';
33
34
  import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
34
- import { WorkflowDomainError } from '../../shared/domain-errors.js';
35
35
  import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
36
36
  import {
37
37
  GROUPS_YAML,
@@ -81,6 +81,12 @@ async function catFileBatch(
81
81
  ): Promise<(string | null)[]> {
82
82
  // Bytes, not text: every `<size>` in the header counts bytes, and the walk
83
83
  // below has to step by that count through content that is not all ASCII.
84
+ // One spec per LINE is the protocol, and this walk depends on getting back
85
+ // exactly as many answers as it sent: a spec carrying a line break would
86
+ // insert an extra answer and shift every result after it, so each directory
87
+ // would be handed the NEXT one's `access.md`. Paths are refused a control
88
+ // character at the normaliser; this is the fence at the protocol.
89
+ assertOneSpecPerLine(specs);
84
90
  const { stdout: out } = await runner.run(repoDir, ['cat-file', '--batch'], {
85
91
  input: `${specs.join('\n')}\n`,
86
92
  encoding: 'buffer',
@@ -951,6 +957,95 @@ function resolveDenialSourcesForVerb(
951
957
  return out;
952
958
  }
953
959
 
960
+ /**
961
+ * The model as it will be once `fromPath` has been moved or copied to
962
+ * `toPath`: every `access.md` the operation carries, keyed at the directory
963
+ * it lands in, since the rules a folder carries travel with its bytes.
964
+ *
965
+ * What travels is BYTES, and the name they land under decides what they
966
+ * govern — `landing` is the caller's answer to that (see
967
+ * `relocationSubject`), already parsed as the destination path:
968
+ *
969
+ * - a FOLDER takes every `access.md` at or under it, each re-keyed to the
970
+ * directory it lands in, and `landing` plays no part;
971
+ * - a FILE governs a directory exactly when it lands as that directory's
972
+ * `access.md`, whatever it was called before — a copy `copy_file` allows,
973
+ * and one that governs the destination folder the moment it lands. Its key
974
+ * is a PARENT directory, never a path under `fromPath`, which is why the
975
+ * prefix walk above cannot see it.
976
+ *
977
+ * The emptied side is the mirror: an `access.md` that MOVES stops governing
978
+ * the directory it left, whether or not it governs anything where it lands.
979
+ *
980
+ * PREVIEW ONLY. Nothing here reads disk and nothing here decides whether an
981
+ * operation may run — the gates resolve against the model as it IS. This
982
+ * answers the other question, the one a dry run asks: what will be true
983
+ * afterwards. Judging the destination as it stands today is what made a
984
+ * rename warn about losing the owner access the folder's own `access.md` was
985
+ * about to hand straight back.
986
+ *
987
+ * `sourceRemains` is the difference between a copy and a move: a copy leaves
988
+ * the rules where they were, a move takes them away. It changes the answer
989
+ * only when the destination resolves through a directory the source
990
+ * occupied; nothing else consults those keys.
991
+ */
992
+ function withRelocatedAccessFiles(
993
+ model: AccessModel,
994
+ fromPath: string,
995
+ toPath: string,
996
+ opts: { sourceRemains: boolean; sourceIsFolder: boolean; landing?: AccessFile | null },
997
+ ): AccessModel {
998
+ /** Directories the operation takes an `access.md` AWAY from. */
999
+ const emptied: string[] = [];
1000
+ /** Directories it leaves one IN, with the file as it will be read there. */
1001
+ const landing: { toDir: string; file: AccessFile }[] = [];
1002
+ if (opts.sourceIsFolder) {
1003
+ for (const [dir, file] of model.accessFilesByDir) {
1004
+ if (dir !== fromPath && !dir.startsWith(`${fromPath}/`)) continue;
1005
+ emptied.push(dir);
1006
+ landing.push({ toDir: `${toPath}${dir.slice(fromPath.length)}`, file });
1007
+ }
1008
+ } else {
1009
+ const fromDir = parentDirOf(fromPath);
1010
+ if (isAccessMdPath(fromPath) && model.accessFilesByDir.has(fromDir)) emptied.push(fromDir);
1011
+ if (opts.landing) landing.push({ toDir: parentDirOf(toPath), file: opts.landing });
1012
+ }
1013
+ if (emptied.length === 0 && landing.length === 0) return model;
1014
+ const accessFilesByDir = new Map(model.accessFilesByDir);
1015
+ if (!opts.sourceRemains) for (const dir of emptied) accessFilesByDir.delete(dir);
1016
+ for (const { toDir, file } of landing) {
1017
+ // A directory that already has an `access.md` keeps it. The operation
1018
+ // would be refused for landing on something that exists long before the
1019
+ // two files could meet, so the model must not invent a merge either.
1020
+ if (accessFilesByDir.has(toDir)) continue;
1021
+ accessFilesByDir.set(toDir, {
1022
+ ...file,
1023
+ dir: toDir,
1024
+ path: toDir ? `${toDir}/access.md` : 'access.md',
1025
+ });
1026
+ }
1027
+ return { ...model, accessFilesByDir };
1028
+ }
1029
+
1030
+ /** The directory a repo-relative file path sits in; `''` for the repo root. */
1031
+ function parentDirOf(relativePath: string): string {
1032
+ const cut = relativePath.lastIndexOf('/');
1033
+ return cut === -1 ? '' : relativePath.slice(0, cut);
1034
+ }
1035
+
1036
+ /**
1037
+ * One file's own-entries, parsed as the path `asName` would be read.
1038
+ *
1039
+ * The same frontmatter means two different things by name: in an `access.md`
1040
+ * it is the FOLDER's rules (and the file governs itself only through the
1041
+ * body format — see {@link accessMdSelfEntries}), and anywhere else it is the
1042
+ * file's own. Which is why a preview must read the bytes as the name they
1043
+ * LAND under, not the one they are leaving.
1044
+ */
1045
+ function ownEntriesFromText(text: string, asName: string): OwnEntries | null {
1046
+ return isAccessMdPath(asName) ? accessMdSelfEntries(text) : parseOwnAccessEntries(text);
1047
+ }
1048
+
954
1049
  /**
955
1050
  * Build the repo-root → path chain of directory scopes that govern
956
1051
  * `relativePath`, e.g. `Knowledge/Sales/Foo.md` →
@@ -1586,16 +1681,25 @@ export class AccessControlService implements IAccessControl {
1586
1681
  }
1587
1682
 
1588
1683
  /**
1589
- * The read and write holders of one file at its current path and at a path
1590
- * it has not moved to yet (see the interface).
1684
+ * The read and write holders of one file or folder at its current path and
1685
+ * at a path it has not moved to yet (see the interface).
1686
+ *
1687
+ * A move changes nothing about what the thing itself carries — a file's
1688
+ * frontmatter and a folder's `access.md` files travel with the bytes — and
1689
+ * everything about the folder chain above it. So the hypothetical side is
1690
+ * the ordinary resolution at the destination path, over what travels:
1591
1691
  *
1592
- * A move changes nothing about the file's own frontmatter — that travels
1593
- * with the bytes — and everything about the folder chain above it. So the
1594
- * hypothetical side is the ordinary resolution with the destination path
1595
- * substituted: the same model, the same own-entries (read from the SOURCE,
1596
- * since nothing sits at the destination to read), resolved against the
1597
- * destination's ancestors. One model load and one file read serve all four
1598
- * lookups.
1692
+ * - a FILE's own-entries, read from the SOURCE (nothing sits at the
1693
+ * destination to read) and parsed as the name they LAND under, layered
1694
+ * over the destination's ancestors — the two sides can differ, because
1695
+ * the same frontmatter is a folder's rules in an `access.md` and the
1696
+ * file's own anywhere else;
1697
+ * - a FOLDER's `access.md` files, keyed at the directories they land in,
1698
+ * and a file landing as a directory's `access.md` at the directory it
1699
+ * lands in (`withRelocatedAccessFiles`). A folder has no frontmatter of
1700
+ * its own, so it passes none.
1701
+ *
1702
+ * One model load and one file read serve all four lookups.
1599
1703
  */
1600
1704
  async prospectiveHolders(
1601
1705
  workspaceId: string,
@@ -1604,31 +1708,64 @@ export class AccessControlService implements IAccessControl {
1604
1708
  ): Promise<ProspectiveHolders> {
1605
1709
  const model = await this.loadModel(workspaceId);
1606
1710
  const repoDir = await this.repoDir(workspaceId);
1607
- // A FILE question only. A folder carries its own `access.md` — which moves
1608
- // with it and governs everything under it — so resolving it as a file
1609
- // would read frontmatter it does not have and omit the rules it does,
1610
- // naming principals that are not the ones a folder move changes. Refuse
1611
- // rather than answer the wrong question convincingly.
1612
- //
1613
- // Only absence is an answer here (see `isAbsence`): a probe that failed on
1614
- // permissions or I/O does not say "this is a file", and folding it into
1615
- // one would let the very case above through on an unreadable source.
1616
- const fromStat = await fs.stat(path.join(repoDir, fromPath)).catch((err: unknown) => {
1617
- if (isAbsence(err)) return null;
1618
- throw err;
1711
+ const { isFolder, ownHere, ownThere, landing } =
1712
+ await this.relocationSubject(repoDir, fromPath, toPath);
1713
+ const after = withRelocatedAccessFiles(model, fromPath, toPath, {
1714
+ sourceRemains: false,
1715
+ sourceIsFolder: isFolder,
1716
+ landing,
1619
1717
  });
1620
- if (fromStat?.isDirectory()) {
1621
- throw new WorkflowDomainError(
1622
- 'prospective access answers for a file, not a folder',
1623
- 400,
1624
- );
1625
- }
1626
- const own = await this.readOwnEntries(repoDir, fromPath);
1627
- const holdersAt = (relativePath: string): PathHolders => ({
1628
- read: eligibleHoldersResolved(model, 'read', relativePath, own),
1629
- write: eligibleHoldersResolved(model, 'write', relativePath, own),
1718
+ const holdersAt = (m: AccessModel, relativePath: string, own: OwnEntries | null): PathHolders => ({
1719
+ read: eligibleHoldersResolved(m, 'read', relativePath, own),
1720
+ write: eligibleHoldersResolved(m, 'write', relativePath, own),
1630
1721
  });
1631
- return { before: holdersAt(fromPath), after: holdersAt(toPath) };
1722
+ return {
1723
+ before: holdersAt(model, fromPath, ownHere),
1724
+ after: holdersAt(after, toPath, ownThere),
1725
+ };
1726
+ }
1727
+
1728
+ /**
1729
+ * The caller's own verbs at `toPath` as they will be once `fromPath` has
1730
+ * been moved or copied there (see the interface).
1731
+ *
1732
+ * Identical to asking `canRead`/`canWrite`/`canDownload`/`canOwner` at
1733
+ * `toPath` whenever nothing travels — an ordinary file with no rules of its
1734
+ * own, or a folder with no `access.md` of its own — so the cases this was
1735
+ * not written for answer exactly as they always did.
1736
+ *
1737
+ * What travels is what `prospectiveHolders` says travels, and for the same
1738
+ * reason: the preview and the dialog's holder lists must not disagree about
1739
+ * one move. A file's own-entries come from the SOURCE, where the bytes
1740
+ * carrying them still are — asking the destination, which does not exist
1741
+ * yet, always answered null and so dropped the frontmatter a rename hands
1742
+ * straight back (requirement 6: what the caller has afterwards equals what
1743
+ * the preview said) — and they are parsed as the name they LAND under,
1744
+ * because that is the parse the gates will use a moment later.
1745
+ */
1746
+ async previewAccessAfterRelocation(
1747
+ workspaceId: string,
1748
+ userEmail: string,
1749
+ fromPath: string,
1750
+ toPath: string,
1751
+ opts?: { sourceRemains?: boolean },
1752
+ ): Promise<PathVerbs> {
1753
+ const repoDir = await this.repoDir(workspaceId);
1754
+ const { isFolder, ownThere: own, landing } =
1755
+ await this.relocationSubject(repoDir, fromPath, toPath);
1756
+ const model = withRelocatedAccessFiles(
1757
+ await this.loadModel(workspaceId),
1758
+ fromPath,
1759
+ toPath,
1760
+ { sourceRemains: opts?.sourceRemains === true, sourceIsFolder: isFolder, landing },
1761
+ );
1762
+ const machineOwned = this.machineOwnedWriteRule(userEmail, toPath);
1763
+ return {
1764
+ read: canReadResolved(model, userEmail, toPath, own),
1765
+ write: machineOwned ?? hasPermissionResolved(model, 'write', userEmail, toPath, own),
1766
+ download: hasPermissionResolved(model, 'download', userEmail, toPath, own),
1767
+ owner: hasPermissionResolved(model, 'owner', userEmail, toPath, own),
1768
+ };
1632
1769
  }
1633
1770
 
1634
1771
  async eligibleWriterEmails(
@@ -2219,8 +2356,69 @@ export class AccessControlService implements IAccessControl {
2219
2356
  } catch {
2220
2357
  return null;
2221
2358
  }
2222
- if (isAccessMdPath(relativePath)) return accessMdSelfEntries(text);
2223
- return parseOwnAccessEntries(text);
2359
+ return ownEntriesFromText(text, relativePath);
2360
+ }
2361
+
2362
+ /**
2363
+ * What a move or a copy of `fromPath` to `toPath` CARRIES — the one disk
2364
+ * read both preview surfaces need:
2365
+ *
2366
+ * - `isFolder`: the source is a folder, so the `access.md` files under it
2367
+ * travel, keyed at the directories they land in;
2368
+ * - `ownHere` / `ownThere`: a file's own-entries as the SOURCE's name reads
2369
+ * them and as the DESTINATION's name will. They differ because the same
2370
+ * frontmatter means two things by name (see `ownEntriesFromText`), so
2371
+ * `Sales/access.md` copied to `Legal/rules-copy.md` arrives as an
2372
+ * ordinary file whose frontmatter is its own — reading it as the source's
2373
+ * name had the preview contradict the copy it was previewing;
2374
+ * - `landing`: the folder rules the bytes WILL be, when they land under the
2375
+ * name `access.md`. Parsed as the destination path, so the directory it
2376
+ * governs is already the right one. An `access.md` that will not parse
2377
+ * travels as nothing, as the loader treats it.
2378
+ *
2379
+ * `lstat`, not `stat`: a SYMLINK is none of this. `move_file` refuses a
2380
+ * symlink source outright, so a link to a directory must not be previewed
2381
+ * as a folder taking access files along, and nothing is read THROUGH the
2382
+ * link either — the link's target is not what would move.
2383
+ *
2384
+ * Only absence is an answer (see `isAbsence`): a probe that failed on
2385
+ * permissions or I/O does not say "this is a file", and reading a folder as
2386
+ * one would resolve it through frontmatter it does not have.
2387
+ */
2388
+ private async relocationSubject(
2389
+ repoDir: string,
2390
+ fromPath: string,
2391
+ toPath: string,
2392
+ ): Promise<{
2393
+ isFolder: boolean;
2394
+ ownHere: OwnEntries | null;
2395
+ ownThere: OwnEntries | null;
2396
+ landing: AccessFile | null;
2397
+ }> {
2398
+ const nothing = { isFolder: false, ownHere: null, ownThere: null, landing: null };
2399
+ const fromStat = await fs.lstat(path.join(repoDir, fromPath)).catch((err: unknown) => {
2400
+ if (isAbsence(err)) return null;
2401
+ throw err;
2402
+ });
2403
+ if (fromStat?.isSymbolicLink() === true) return nothing;
2404
+ if (fromStat?.isDirectory() === true) return { ...nothing, isFolder: true };
2405
+ // `roles.yaml` carries no own-entries wherever it sits, exactly as
2406
+ // `readOwnEntries` has it; a read that fails for any reason is no rules,
2407
+ // which is what the gates would say about the same file.
2408
+ if (fromPath === 'roles.yaml' || toPath === 'roles.yaml') return nothing;
2409
+ let text: string;
2410
+ try {
2411
+ text = await fs.readFile(path.join(repoDir, fromPath), 'utf-8');
2412
+ } catch {
2413
+ return nothing;
2414
+ }
2415
+ const parsed = isAccessMdPath(toPath) ? parseAccessFile(text, toPath) : null;
2416
+ return {
2417
+ isFolder: false,
2418
+ ownHere: ownEntriesFromText(text, fromPath),
2419
+ ownThere: ownEntriesFromText(text, toPath),
2420
+ landing: parsed?.ok === true ? parsed.file : null,
2421
+ };
2224
2422
  }
2225
2423
 
2226
2424
  /**
@@ -368,27 +368,25 @@ export function createAccessRoutes(
368
368
  });
369
369
 
370
370
  /**
371
- * GET /api/workspace/:id/access/prospective?from=<file>&toDir=<folder>
371
+ * GET /api/workspace/:id/access/prospective?from=<file|folder>&toDir=<folder>
372
372
  *
373
- * Who can open and who can edit one file where it is, and where a move
374
- * would put it — `{ before, after }`, each `{ read, write }` lists of
375
- * principals named as their grants name them. `toDir` is the destination
376
- * FOLDER (`''` is the repo root); the file keeps its name, so the route
377
- * derives the destination path itself rather than trusting a second one.
373
+ * Who can open and who can edit one file OR FOLDER where it is, and where
374
+ * a move would put it — `{ before, after }`, each `{ read, write }` lists
375
+ * of principals named as their grants name them. `toDir` is the
376
+ * destination FOLDER (`''` is the repo root); the thing keeps its name, so
377
+ * the route derives the destination path itself rather than trusting a
378
+ * second one.
378
379
  *
379
380
  * The destination path does not exist yet, which is why this cannot be two
380
- * calls to `GET /access`: the resolver is asked for a hypothetical, with
381
- * the file's own frontmatter (read where the file actually is) layered over
382
- * the destination's folder chain. Nothing is written and nothing is moved.
381
+ * calls to `GET /access`: the resolver is asked for a hypothetical, over
382
+ * what the move carries — a file's own frontmatter, or the `access.md`
383
+ * files inside a folder, at the paths they land on. Nothing is written and
384
+ * nothing is moved.
383
385
  *
384
386
  * Gated like the sibling `overrides` route rather than the permissive
385
- * `GET /access`: the caller must resolve read on the file being moved. The
386
- * lists name people, and someone who cannot see the file has no business
387
+ * `GET /access`: the caller must resolve read on the thing being moved. The
388
+ * lists name people, and someone who cannot see it has no business
387
389
  * learning who can.
388
- *
389
- * `from` must be a FILE. A folder carries its own `access.md` and governs
390
- * everything under it, which is a different question; the resolver refuses
391
- * one with a 400 rather than answering it as if it were a file.
392
390
  */
393
391
  router.get('/workspace/:id/access/prospective', async (req, res) => {
394
392
  const user = await requireUser(req, res);
@@ -726,10 +724,13 @@ export function createAccessRoutes(
726
724
  async function accountsAmong(emails: string[]): Promise<Set<string>> {
727
725
  // `inArray` refuses an empty list, and there is nothing to ask anyway.
728
726
  if (emails.length === 0) return new Set();
727
+ // Through the blind index: `email` is randomized ciphertext, and the
728
+ // index is what the unique constraint and every lookup key on. It is
729
+ // compared with the addresses themselves.
729
730
  const rows = await db
730
731
  .select({ email: users.email })
731
732
  .from(users)
732
- .where(inArray(users.email, emails));
733
+ .where(inArray(users.emailBidx, emails));
733
734
  return new Set(rows.map((r) => r.email.trim().toLowerCase()));
734
735
  }
735
736
 
@@ -26,8 +26,12 @@ export const DIRECTORY_SYNC_BOT_NAME = 'Directory Sync Bot';
26
26
  export async function ensureDirectorySyncBot(db: Database): Promise<AuthUser> {
27
27
  const inserted = await db
28
28
  .insert(users)
29
- .values({ email: DIRECTORY_SYNC_BOT_EMAIL, name: DIRECTORY_SYNC_BOT_NAME })
30
- .onConflictDoNothing({ target: users.email })
29
+ .values({
30
+ email: DIRECTORY_SYNC_BOT_EMAIL,
31
+ emailBidx: DIRECTORY_SYNC_BOT_EMAIL,
32
+ name: DIRECTORY_SYNC_BOT_NAME,
33
+ })
34
+ .onConflictDoNothing({ target: users.emailBidx })
31
35
  .returning();
32
36
  if (inserted.length > 0) {
33
37
  const row = inserted[0];
@@ -37,7 +41,7 @@ export async function ensureDirectorySyncBot(db: Database): Promise<AuthUser> {
37
41
  const [row] = await db
38
42
  .select()
39
43
  .from(users)
40
- .where(eq(users.email, DIRECTORY_SYNC_BOT_EMAIL))
44
+ .where(eq(users.emailBidx, DIRECTORY_SYNC_BOT_EMAIL))
41
45
  .limit(1);
42
46
  if (!row) {
43
47
  throw new Error(
@@ -6,7 +6,11 @@ import type { IExternalApiKeyService } from '../../tool-auth/external-api-key.in
6
6
  import type { InternalTokenService } from '../../tool-auth/internal-token.service.js';
7
7
  import type { AuthService } from '../../auth/auth.service.js';
8
8
  import { createAgentInstructionsRoutes } from '../agent-instructions.routes.js';
9
- import { PLATFORM_HEADER, TOOL_PREFIX_LINE } from '../compose.js';
9
+ import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
10
+ import { TOOL_PREFIX_LINE, platformInstructions } from '../compose.js';
11
+
12
+ /** The platform-owned text the route leads with: header + the shared file rules. */
13
+ const PLATFORM = platformInstructions(DEFAULT_KB_LAYOUT);
10
14
 
11
15
  /**
12
16
  * `GET /agent/instructions` behind the REAL manual-auth gate, with the three
@@ -31,11 +35,13 @@ async function serve(readPreamble: () => Promise<string | null>): Promise<string
31
35
  looksLikeInternalToken: (t: string) => t.startsWith('bit_'),
32
36
  verify: (t: string) => (t === INTERNAL_TOKEN ? { userId: 'u-internal' } : null),
33
37
  } as unknown as InternalTokenService;
38
+ // Sessions and internal tokens are accepted only for an account that is on; these are.
34
39
  const authService = {
35
- verifyToken: (t: string) => {
40
+ resolveSession: async (t: string) => {
36
41
  if (t !== BROWSER_JWT) throw new Error('bad jwt');
37
42
  return { userId: 'u-browser' };
38
43
  },
44
+ isActive: async () => true,
39
45
  } as unknown as AuthService;
40
46
  const app = express();
41
47
  app.use('/api', createAgentInstructionsRoutes(createManualAuthMiddleware(externalKeys, internalTokens, authService), readPreamble));
@@ -59,7 +65,7 @@ describe('GET /agent/instructions', () => {
59
65
  expect(res.status, bearer).toBe(200);
60
66
  expect(res.headers.get('cache-control')).toBe('no-store');
61
67
  const body = (await res.json()) as Record<string, unknown>;
62
- expect(body.instructions).toBe(`${PLATFORM_HEADER}\n\nAcme builds solar farms.`);
68
+ expect(body.instructions).toBe(`${PLATFORM}\n\nAcme builds solar farms.`);
63
69
  expect(body.toolPrefix).toBe(`${TOOL_PREFIX_LINE} Acme builds solar farms.`);
64
70
  expect(body).toMatchObject({
65
71
  truncated: false,
@@ -77,12 +83,12 @@ describe('GET /agent/instructions', () => {
77
83
  expect((await fetch(url, { headers: { Authorization: 'Bearer not-a-session' } })).status).toBe(401);
78
84
  });
79
85
 
80
- it('a missing file yields the header alone', async () => {
86
+ it('a missing file yields the platform text alone', async () => {
81
87
  const url = await serve(async () => null);
82
88
  const res = await fetch(url, { headers: { Authorization: `Bearer ${CONNECTION_KEY}` } });
83
89
  expect(res.status).toBe(200);
84
90
  const body = (await res.json()) as { instructions: string; toolPrefix: string; preambleChars: number };
85
- expect(body.instructions).toBe(PLATFORM_HEADER);
91
+ expect(body.instructions).toBe(PLATFORM);
86
92
  expect(body.toolPrefix).toBe(TOOL_PREFIX_LINE);
87
93
  expect(body.preambleChars).toBe(0);
88
94
  });
@@ -1,6 +1,8 @@
1
+ import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import {
3
4
  PLATFORM_HEADER,
5
+ platformInstructions,
4
6
  PREAMBLE_CAP,
5
7
  PREAMBLE_TRUNCATION_MARKER,
6
8
  TOOL_PREFIX_CAP,
@@ -13,13 +15,19 @@ import {
13
15
  * The composer is pure, so every rule of the two texts is pinned here: what
14
16
  * an absent or empty preamble yields, how comments are withheld, where both
15
17
  * caps cut, and that a cut never splits a character.
18
+ *
19
+ * `PLATFORM` is everything the code owns and the admin cannot edit: the header
20
+ * and the rules every file tool shares, which is what the handshake leads with.
21
+ * The rules themselves are pinned in `shared-file-rules.test.ts`; here they are
22
+ * only the fixed text the preamble follows.
16
23
  */
24
+ const PLATFORM = platformInstructions(DEFAULT_KB_LAYOUT);
17
25
 
18
26
  describe('composeAgentInstructions: the handshake text', () => {
19
- it('sends the header alone for an absent, empty, whitespace or comment-only preamble', () => {
27
+ it('sends the platform text alone — header and shared rules — for an absent, empty, whitespace or comment-only preamble', () => {
20
28
  for (const raw of [null, '', ' \n\n', '<!-- notes to myself -->', '<!--\nline one\nline two\n-->\n']) {
21
29
  const out = composeAgentInstructions(raw);
22
- expect(out.instructions, JSON.stringify(raw)).toBe(PLATFORM_HEADER);
30
+ expect(out.instructions, JSON.stringify(raw)).toBe(PLATFORM);
23
31
  expect(out.toolPrefix).toBe(TOOL_PREFIX_LINE);
24
32
  expect(out.truncated).toBe(false);
25
33
  expect(out.toolPrefixTruncated).toBe(false);
@@ -28,15 +36,16 @@ describe('composeAgentInstructions: the handshake text', () => {
28
36
  }
29
37
  });
30
38
 
31
- it('is the header, a blank line, then the preamble body', () => {
39
+ it('is the platform text, a blank line, then the preamble body', () => {
32
40
  const out = composeAgentInstructions('We sell permits.\n\nLook in Permitting/ first.\n');
33
- expect(out.instructions).toBe(`${PLATFORM_HEADER}\n\nWe sell permits.\n\nLook in Permitting/ first.`);
41
+ expect(out.instructions).toBe(`${PLATFORM}\n\nWe sell permits.\n\nLook in Permitting/ first.`);
34
42
  expect(out.preambleChars).toBe('We sell permits.\n\nLook in Permitting/ first.'.length);
35
43
  });
36
44
 
37
45
  it('hands the two parts and the fixed prefix line back separately, for the card', () => {
38
46
  const out = composeAgentInstructions('We sell permits.');
39
- expect(out.header).toBe(PLATFORM_HEADER);
47
+ expect(out.header).toBe(PLATFORM);
48
+ expect(out.header.startsWith(PLATFORM_HEADER)).toBe(true);
40
49
  expect(out.preamble).toBe('We sell permits.');
41
50
  expect(out.toolPrefixLine).toBe(TOOL_PREFIX_LINE);
42
51
  expect(out.toolPrefix.startsWith(out.toolPrefixLine)).toBe(true);
@@ -56,7 +65,7 @@ describe('composeAgentInstructions: the handshake text', () => {
56
65
 
57
66
  it('an unterminated comment withholds everything after it and is reported', () => {
58
67
  const out = composeAgentInstructions('Visible.\n\n<!-- forgot to close\nSecret folder names\n');
59
- expect(out.instructions).toBe(`${PLATFORM_HEADER}\n\nVisible.`);
68
+ expect(out.instructions).toBe(`${PLATFORM}\n\nVisible.`);
60
69
  expect(out.instructions).not.toContain('Secret');
61
70
  expect(out.toolPrefix).toBe(`${TOOL_PREFIX_LINE} Visible.`);
62
71
  expect(out.unterminatedComment).toBe(true);
@@ -66,12 +75,12 @@ describe('composeAgentInstructions: the handshake text', () => {
66
75
  const exact = 'a'.repeat(PREAMBLE_CAP);
67
76
  const atCap = composeAgentInstructions(exact);
68
77
  expect(atCap.truncated).toBe(false);
69
- expect(atCap.instructions).toBe(`${PLATFORM_HEADER}\n\n${exact}`);
78
+ expect(atCap.instructions).toBe(`${PLATFORM}\n\n${exact}`);
70
79
  expect(atCap.instructions).not.toContain('truncated');
71
80
 
72
81
  const over = composeAgentInstructions(`${exact}b`);
73
82
  expect(over.truncated).toBe(true);
74
- expect(over.instructions).toBe(`${PLATFORM_HEADER}\n\n${exact}\n${PREAMBLE_TRUNCATION_MARKER}`);
83
+ expect(over.instructions).toBe(`${PLATFORM}\n\n${exact}\n${PREAMBLE_TRUNCATION_MARKER}`);
75
84
  expect(PREAMBLE_TRUNCATION_MARKER).toContain('6,000');
76
85
  expect(PREAMBLE_TRUNCATION_MARKER).toContain('mcp-description.md');
77
86
  });
@@ -93,7 +102,7 @@ describe('composeAgentInstructions: the handshake text', () => {
93
102
  const raw = `${'a'.repeat(PREAMBLE_CAP - 1)}😀tail`;
94
103
  const out = composeAgentInstructions(raw);
95
104
  expect(out.truncated).toBe(true);
96
- const body = out.instructions.slice(PLATFORM_HEADER.length + 2);
105
+ const body = out.instructions.slice(PLATFORM.length + 2);
97
106
  const sent = body.slice(0, body.indexOf('\n'));
98
107
  expect(sent).toBe('a'.repeat(PREAMBLE_CAP - 1));
99
108
  expect(sent.length).toBe(PREAMBLE_CAP - 1);
@@ -222,7 +231,7 @@ describe('composeAgentInstructions: the tool prefix', () => {
222
231
  it('CRLF input still yields the first paragraph', () => {
223
232
  const out = composeAgentInstructions('# Title\r\n\r\nFirst paragraph.\r\n\r\nSecond.\r\n');
224
233
  expect(out.toolPrefix).toBe(`${TOOL_PREFIX_LINE} First paragraph.`);
225
- expect(out.instructions).toBe(`${PLATFORM_HEADER}\n\n# Title\n\nFirst paragraph.\n\nSecond.`);
234
+ expect(out.instructions).toBe(`${PLATFORM}\n\n# Title\n\nFirst paragraph.\n\nSecond.`);
226
235
  });
227
236
 
228
237
  it('is at most the cap, still starting with the fixed line, and reports the count before the cut', () => {