@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,164 @@
1
+ /**
2
+ * Agent access gate — the per-call seam every gated agent tool runs through.
3
+ *
4
+ * It decides nothing of its own. It calls the hooks a DEPLOYMENT registered
5
+ * (`WorkflowHooks`): one before an agent read of a knowledge-base path, one
6
+ * before an agent write. A hook that throws refuses the operation and the
7
+ * caller sees that hook's own message and status. With no hook registered —
8
+ * every core-only deployment — nothing is refused and nothing is recorded, and
9
+ * a call that carries no `sessionId` is an ordinary call.
10
+ *
11
+ * The gate owns exactly two skips, and they are the ones core can decide on
12
+ * its own identity model: the caller is not an agent (a person in the app, a
13
+ * browser JWT), or the caller is the recovery/merge bot. Everything else —
14
+ * which paths matter, whether a session id is required, what the refusal says
15
+ * — belongs to the hook.
16
+ *
17
+ * Lives beside `assertCanRead` in the workspace tools; it does NOT touch
18
+ * `acquireLock` (that gate is protected-branch-only, and this one applies on
19
+ * all branches and to reads).
20
+ */
21
+
22
+ import type { JsonSchema } from '../tool-registry/tool.contract.js';
23
+ import type { ToolContext } from '../tool-helpers/tool.contract.js';
24
+ import type { WorkflowHooks } from '../workflow/workflow-hooks.js';
25
+
26
+ /**
27
+ * What the `sessionId` input says on its own, before any deployment note. The
28
+ * id is the CONVERSATION's id: the in-process agent supplies its thread id
29
+ * automatically (via its per-run internal token), and an external agent mints
30
+ * one with `start_session` and passes it explicitly, because the MCP proxy is
31
+ * a pure passthrough and never injects args. The same id is the one `ask`
32
+ * takes, so a run's reads and its questions share one conversation.
33
+ */
34
+ export const SESSION_ID_DESCRIPTION =
35
+ "This conversation's id, so the server can tell one run's calls apart. External agents (direct MCP calls and `call_tool_chain` alike): call the `start_session` tool ONCE at the very start of your work to obtain it, then pass that id here — the same id `ask` takes. The in-process agent runtime supplies this automatically (do not set it yourself).";
36
+
37
+ /**
38
+ * The `sessionId` input every gated tool declares. The description a MOUNTED
39
+ * tool ends up advertising is {@link ToolDescriptionNotes.sessionIdDescription},
40
+ * which appends whatever note the deployment registered; this constant is the
41
+ * unadorned default the notes are applied on top of.
42
+ */
43
+ export const SESSION_ID_INPUT: JsonSchema = {
44
+ type: 'string',
45
+ description: SESSION_ID_DESCRIPTION,
46
+ };
47
+
48
+ /**
49
+ * The registration point for the wording a DEPLOYMENT wants agents to read
50
+ * about the gated tools — the counterpart of the hooks, which carry its rule.
51
+ * A deployment that refuses some calls can say so here; core registers
52
+ * nothing, so a core-only deployment advertises the plain descriptions.
53
+ *
54
+ * Registration may land AFTER the tools are mounted (an overlay registers from
55
+ * the tool-surface hook), so mounted tools subscribe with {@link onChange} and
56
+ * rewrite their own description and `sessionId` input when a note arrives.
57
+ */
58
+ export class ToolDescriptionNotes {
59
+ private gatedTool = '';
60
+ private sessionId = '';
61
+ private readonly listeners = new Set<() => void>();
62
+
63
+ /** Append `note` to the description of every gated tool. */
64
+ registerGatedToolNote(note: string): void {
65
+ this.gatedTool = note;
66
+ this.announce();
67
+ }
68
+
69
+ /** Append `note` to the description of the `sessionId` input. */
70
+ registerSessionIdNote(note: string): void {
71
+ this.sessionId = note;
72
+ this.announce();
73
+ }
74
+
75
+ /** The registered gated-tool note, `''` when none is registered. */
76
+ gatedToolNote(): string {
77
+ return this.gatedTool;
78
+ }
79
+
80
+ /** The `sessionId` input's description, with any registered note appended. */
81
+ sessionIdDescription(): string {
82
+ return SESSION_ID_DESCRIPTION + this.sessionId;
83
+ }
84
+
85
+ /** Re-run `listener` whenever a note is registered (mounted tools re-describe). */
86
+ onChange(listener: () => void): void {
87
+ this.listeners.add(listener);
88
+ }
89
+
90
+ private announce(): void {
91
+ for (const listener of this.listeners) listener();
92
+ }
93
+ }
94
+
95
+ /** The per-deployment configuration the gate needs, built once in the composition root. */
96
+ export interface AgentAccessGate {
97
+ /** Lowercased email of the recovery/merge bot, which never reaches the hooks. */
98
+ recoveryBotEmail: string;
99
+ /**
100
+ * The workflow lifecycle-hook registry holding the BLOCKING `agentRead` and
101
+ * `preWrite` hooks. Core registers none; a deployment registers against
102
+ * `workflowService.hooks` — pass that same instance here.
103
+ */
104
+ hooks: WorkflowHooks;
105
+ /** The notes a deployment adds to what agents read about these tools. */
106
+ notes: ToolDescriptionNotes;
107
+ }
108
+
109
+ /** Only agent callers are gated; `session` (browser JWT) and humans are not. */
110
+ function agentSourceOf(ctx: ToolContext): 'internal' | 'external' | null {
111
+ return ctx.source === 'internal' || ctx.source === 'external' ? ctx.source : null;
112
+ }
113
+
114
+ /**
115
+ * The hook context for this call, or `null` when the call never reaches a hook:
116
+ * the caller is a person rather than an agent, or is the recovery/merge bot,
117
+ * which legitimately spans everything.
118
+ */
119
+ function operationContext(
120
+ gate: AgentAccessGate,
121
+ ctx: ToolContext,
122
+ branch: string,
123
+ wsPath?: string,
124
+ ): { sessionId?: string; wsPath?: string; branch: string; user: ToolContext['user']; source: 'internal' | 'external' } | null {
125
+ const source = agentSourceOf(ctx);
126
+ if (source === null) return null;
127
+ if (ctx.user.email.toLowerCase() === gate.recoveryBotEmail) return null;
128
+ return { sessionId: ctx.sessionId, wsPath, branch, user: ctx.user, source };
129
+ }
130
+
131
+ /**
132
+ * Tell the registered read hooks about an agent read of `wsPath`. A hook that
133
+ * throws refuses the read with its own message and status; with none
134
+ * registered this is a no-op.
135
+ */
136
+ export async function notifyAgentRead(
137
+ gate: AgentAccessGate,
138
+ ctx: ToolContext,
139
+ branch: string,
140
+ wsPath: string,
141
+ ): Promise<void> {
142
+ const op = operationContext(gate, ctx, branch, wsPath);
143
+ if (op === null) return;
144
+ await gate.hooks.runAgentRead(op);
145
+ }
146
+
147
+ /**
148
+ * Ask the registered write hooks whether an agent write may proceed. A hook
149
+ * that throws refuses it with its own message and status; with none registered
150
+ * this is a no-op.
151
+ *
152
+ * `wsPath` is omitted by the one write-capable tool with no resolvable target,
153
+ * `execute_command` — the hook then decides at the coarser session level.
154
+ */
155
+ export async function assertAgentWriteAllowed(
156
+ gate: AgentAccessGate,
157
+ ctx: ToolContext,
158
+ branch: string,
159
+ wsPath?: string,
160
+ ): Promise<void> {
161
+ const op = operationContext(gate, ctx, branch, wsPath);
162
+ if (op === null) return;
163
+ await gate.hooks.runPreWrite(op);
164
+ }
@@ -0,0 +1,214 @@
1
+ import express from 'express';
2
+ import { validateFilename } from '@bevel-software/platform-shared';
3
+ import { logger } from '../../shared/logging.js';
4
+ import { AgentUploadStore, UploadTokenError, overLimit } from './agent-upload.store.js';
5
+
6
+ const log = logger('agent-uploads');
7
+
8
+ /** The route's own prefix, under `/api`. One spelling, shared with the raw-body test below. */
9
+ export const AGENT_UPLOAD_ROUTE = '/agent/uploads/:token';
10
+
11
+ /**
12
+ * The same endpoint with the token in the `x-upload-token` HEADER instead of
13
+ * the path — the address `uploadUrl` is, without its last segment.
14
+ *
15
+ * Two spellings because a token in a URL is a credential in a place that keeps
16
+ * copies: an access log, a proxy log, a shell history, a `ps` listing of the
17
+ * `curl` that sent it. The path form is what `request_file_upload` answers,
18
+ * because one address an agent can paste into any client is the thing that
19
+ * makes this route usable at all; the header form is for a caller that would
20
+ * rather its credential not be written down on the way. Both reach the same
21
+ * handler and are judged identically — same token, same single use.
22
+ */
23
+ export const AGENT_UPLOAD_HEADER_ROUTE = '/agent/uploads';
24
+
25
+ /**
26
+ * Whether `path` is the agent upload route, and so must reach its handler with
27
+ * the body still a STREAM.
28
+ *
29
+ * The app installs a global `express.json()`. It only claims a JSON
30
+ * content-type, but `curl --data-binary @file.zip -H 'content-type:
31
+ * application/json'` is a request an agent can and will make — and once the
32
+ * parser has drained the stream there are no bytes left to store, so the
33
+ * upload would answer "0 bytes received" for a file that was sent in full.
34
+ * Exempting the path is the same move `/api/sync` makes for its HMAC body, and
35
+ * for the same reason: whoever needs the exact bytes has to see them first.
36
+ *
37
+ * Case-insensitive, because Express routing is: `/api/Agent/uploads/x` reaches
38
+ * this router, and a check that said no would let the parser eat that body.
39
+ */
40
+ export function isAgentUploadRawBodyPath(path: string): boolean {
41
+ // Trailing slashes trimmed first, so the header form's bare address matches
42
+ // in every spelling Express routes to it (`/api/agent/uploads` and
43
+ // `/api/agent/uploads/` are one route): missing one of them would hand that
44
+ // request to the JSON parser, and the bytes it drains are gone.
45
+ const lower = path.toLowerCase().replace(/\/+$/, '');
46
+ return lower === '/api/agent/uploads' || lower.startsWith('/api/agent/uploads/');
47
+ }
48
+
49
+ export interface AgentUploadRouteDeps {
50
+ uploads: AgentUploadStore;
51
+ }
52
+
53
+ /**
54
+ * `POST /api/agent/uploads/:token` — the one endpoint on this server
55
+ * authenticated by a single-use token and nothing else.
56
+ *
57
+ * It exists because MCP tool arguments are JSON, so every byte an agent sends
58
+ * through a tool passes through the model first. A 37 KB page gets truncated
59
+ * mid-response; a file of regex backslashes fails to parse as a JSON string; a
60
+ * PNG cannot be sent at all. Here the agent asks for a token, sends the file
61
+ * with any HTTP client it has, and then names the token in
62
+ * `apply_file_upload`. The bytes never enter a prompt.
63
+ *
64
+ * What the route itself may do is deliberately almost nothing: it stores bytes
65
+ * against a token, in a directory beside the workspaces root, and answers what
66
+ * it received. It resolves no workspace, writes nothing into one, and commits
67
+ * nothing — every access, platform-file and branch rule is applied later, by
68
+ * the apply tool, against the branch the caller then names. A token that is
69
+ * unknown, spent, expired or someone else's gets one 404 that says which of
70
+ * those it was: none of them.
71
+ */
72
+ export function createAgentUploadRoutes(deps: AgentUploadRouteDeps): express.Router {
73
+ const router = express.Router();
74
+ const { uploads } = deps;
75
+
76
+ const handle: express.RequestHandler = async (req, res) => {
77
+ const token = tokenOf(req);
78
+ try {
79
+ // THE TOKEN FIRST, before anything about the request is read, parsed,
80
+ // judged or quoted back. This route is authenticated by the token and
81
+ // nothing else, which cuts two ways. Without the check up here, anyone
82
+ // could make the process buffer the deployment's whole upload limit per
83
+ // request against tokens they invented, as many at a time as they liked;
84
+ // and a caller holding no token could learn which of its OTHER guesses
85
+ // were well formed — "that is not a usable file name" is an answer only
86
+ // somebody entitled to send a file should get. One refusal, nothing else.
87
+ // `receive` below asks again, under the same record, because that is
88
+ // where the token is actually spent.
89
+ uploads.assertOpen(token);
90
+ const filename = fileNameOf(req);
91
+ if (filename === null) {
92
+ res.status(400).json({
93
+ error:
94
+ 'Name the file you are sending: add `?filename=<name>` to the upload URL (or send it as the ' +
95
+ '`x-upload-filename` header). A single file lands under that name, and a name ending in `.zip` is ' +
96
+ 'read as an archive.',
97
+ });
98
+ return;
99
+ }
100
+ const invalid = validateFilename(filename);
101
+ if (invalid !== null || filename.includes('/')) {
102
+ res.status(400).json({
103
+ error: `"${filename}" is not a usable file name: ${invalid ?? 'a name cannot contain "/"'}. Send one plain file name.`,
104
+ });
105
+ return;
106
+ }
107
+ // Then the declared length, so a caller sending something far too large
108
+ // is told the limit before it spends the bandwidth. The real total is
109
+ // counted by the store as the bytes arrive — `content-length` is the
110
+ // sender's claim, not a fact.
111
+ const declared = Number.parseInt(req.headers['content-length'] ?? '', 10);
112
+ if (Number.isFinite(declared) && declared > uploads.maxBytes) {
113
+ res.status(413).json({ error: overLimit(declared, uploads.maxBytes) });
114
+ return;
115
+ }
116
+ // The request itself, as a stream: the store writes the bytes to disk as
117
+ // they arrive and never holds the body. It refuses an empty body, one
118
+ // past the limit and an unreadable archive with its own status.
119
+ res.json(await uploads.receive(token, filename, req));
120
+ } catch (err) {
121
+ if (err instanceof UploadTokenError) {
122
+ res.status(err.status).json({ error: err.message });
123
+ return;
124
+ }
125
+ // The detail stays in the log. An unexpected failure here is a
126
+ // filesystem error, and its message quotes the absolute path of the
127
+ // store's root — a place the caller is told nothing else about.
128
+ log.error('upload failed:', { err });
129
+ res.status(500).json({ error: 'Upload failed' });
130
+ }
131
+ };
132
+
133
+ // Both spellings of the one endpoint: the token in the path, or in the
134
+ // `x-upload-token` header on the bare address.
135
+ router.post(AGENT_UPLOAD_ROUTE, handle);
136
+ router.post(AGENT_UPLOAD_HEADER_ROUTE, handle);
137
+
138
+ return router;
139
+ }
140
+
141
+ /**
142
+ * The token the sender presented: the `:token` path segment, or the
143
+ * `x-upload-token` header when the bytes went to the bare address.
144
+ *
145
+ * The path wins when both are present — it is the address the sender actually
146
+ * POSTed to, and a header left over from an earlier upload must not quietly
147
+ * redirect these bytes onto a different token.
148
+ *
149
+ * No token at all answers the empty string rather than its own refusal, so it
150
+ * goes through `assertOpen` like any other unusable token and gets the same
151
+ * single 404. A caller holding nothing is told nothing it did not already
152
+ * know — not even whether this endpoint wanted a header.
153
+ */
154
+ function tokenOf(req: express.Request): string {
155
+ const inPath = req.params.token;
156
+ if (typeof inPath === 'string' && inPath.trim() !== '') return inPath.trim();
157
+ const header = req.headers['x-upload-token'];
158
+ if (typeof header === 'string' && header.trim() !== '') return header.trim();
159
+ return '';
160
+ }
161
+
162
+ /**
163
+ * The name the sender gave the file: the `filename` query parameter, the
164
+ * `x-upload-filename` header, or a `content-disposition`'s own `filename=`.
165
+ * Three spellings because three kinds of client are expected to use this —
166
+ * `curl` with a query string, a scripted `fetch` with a header, and a client
167
+ * that sends the disposition it would send to any upload endpoint.
168
+ */
169
+ function fileNameOf(req: express.Request): string | null {
170
+ const query = req.query.filename;
171
+ if (typeof query === 'string' && query.trim() !== '') return query.trim();
172
+ const header = req.headers['x-upload-filename'];
173
+ if (typeof header === 'string' && header.trim() !== '') return header.trim();
174
+ const disposition = req.headers['content-disposition'];
175
+ if (typeof disposition === 'string') return dispositionFilename(disposition);
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * The `filename` a `content-disposition` names, or null when it names none.
181
+ *
182
+ * A QUOTED value is read to its closing quote, not to the first semicolon: a
183
+ * semicolon separates the header's parameters only OUTSIDE the quotes, and
184
+ * `filename="report;final.md"` is one perfectly ordinary name that a
185
+ * semicolon-first reading landed as `report`. An unquoted value ends at the
186
+ * next parameter, as it must.
187
+ *
188
+ * Percent-decoded only in the extended `filename*=UTF-8''…` form, which is the
189
+ * only one where the encoding is part of the grammar. A plain `filename=` value
190
+ * is the name itself, so `50%20off.md` stays `50%20off.md` rather than losing
191
+ * its `%20` to a decode nobody asked for.
192
+ *
193
+ * The parameter name is matched at a boundary — the start of the header or a
194
+ * `;` — because `filename` is a suffix of other perfectly legal parameter
195
+ * names: without it, `inline; xfilename=wrong.md` read `wrong.md` as the name
196
+ * the sender gave, from a parameter that says nothing of the kind.
197
+ */
198
+ export function dispositionFilename(disposition: string): string | null {
199
+ const match = /(?:^|;)\s*filename(\*?)\s*=\s*(?:"([^"]*)"|([^;]*))/i.exec(disposition);
200
+ if (!match) return null;
201
+ const extended = match[1] === '*';
202
+ const raw = (match[2] ?? match[3] ?? '').trim();
203
+ if (!extended) return raw === '' ? null : raw;
204
+ // `UTF-8''name`, or any other charset and language the sender declares.
205
+ const encoded = raw.replace(/^[^']*'[^']*'/, '');
206
+ try {
207
+ const decoded = decodeURIComponent(encoded).trim();
208
+ if (decoded !== '') return decoded;
209
+ } catch {
210
+ const kept = encoded.trim();
211
+ if (kept !== '') return kept;
212
+ }
213
+ return null;
214
+ }