@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
@@ -4,17 +4,21 @@ const log = logger('workspace');
4
4
  import path from 'node:path';
5
5
  import AdmZip from 'adm-zip';
6
6
  import { AsyncLocalStorage } from 'node:async_hooks';
7
- import { validateRelativePath, validateFilename, FOLDER_PLACEHOLDER, isFolderPlaceholder, entryExistsMessage, } from '@bevel-software/platform-shared';
7
+ import { validateRelativePath, FOLDER_PLACEHOLDER, isFolderPlaceholder, entryExistsMessage, } from '@bevel-software/platform-shared';
8
8
  import { isAbsence } from '../../shared/fs.contract.js';
9
9
  import { DestinationTakenError, inspectDestination, renameNoReplace, } from '../../shared/rename-no-replace.js';
10
10
  import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
11
11
  import { assertWithinDirectory } from '../../shared/path-containment.js';
12
12
  import { assertRepoRootNameFree, normalizeWorkspacePath } from '../kb-fs/repo-path.js';
13
13
  import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
14
+ import { isZipNoiseEntry, readZipEntry, zipEntryName, zipEntryNameRefusal, zipEntrySegments } from './zip-entry-rules.js';
14
15
  import { GitInternalsError, PathNotFoundError, PathTraversalError, UnreadableArchiveError, WorkflowValidationError, } from '../../shared/domain-errors.js';
15
16
  import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
16
17
  import { BranchNotFoundError, RemoteBranchGoneError, WorkflowDomainError, isMissingRemoteBranchFailure, } from '../../shared/domain-errors.js';
17
18
  import { assertValidBranchName } from '../kb-fs/branch-name.js';
19
+ import { normalizeRepositoryAddress, sameRepository } from '../kb-fs/remote-url.js';
20
+ import { redactSecret, urlQuerySecrets } from '../../shared/redact-secret.js';
21
+ import { setAsideClone, setAsideRootFor, setAsideStamp } from './set-aside-clone.js';
18
22
  import { cloneCredentialArgs, cloneCredentialConfigArgs, cloneTrackingConfigArgs, SAFE_IMPLICIT_FETCH_ARGS, } from '../kb-fs/clone-config.js';
19
23
  // Zip-bomb guardrails. The upload cap is 50 MB compressed; even a generous
20
24
  // 100× expansion ratio still fits inside these limits, so a well-behaved
@@ -142,6 +146,8 @@ export class WorkspaceService {
142
146
  kb;
143
147
  disk;
144
148
  gitRunner;
149
+ setAsideRoot;
150
+ aroundSetAside;
145
151
  /** Maps branch → absolute directory path. Lazily populated. */
146
152
  branchDirs = new Map();
147
153
  /**
@@ -213,13 +219,41 @@ export class WorkspaceService {
213
219
  * runner on its default deadline and no token, for a directly
214
220
  * constructed service.
215
221
  */
216
- gitRunner = new NodeGitRunner()) {
222
+ gitRunner = new NodeGitRunner(),
223
+ /**
224
+ * Where a working copy of a repository that is no longer the configured
225
+ * one is kept. THE SAME VALUE THE KB STARTUP PHASE IS GIVEN — what the two
226
+ * of them move must end up in one place, and omitting it here falls back to
227
+ * a directory that does not survive a deployment's next recreate.
228
+ *
229
+ * Why, in one place: `setAsideRootFor` in `set-aside-clone.ts`.
230
+ */
231
+ setAsideRoot,
232
+ /**
233
+ * What surrounds the setting aside of ONE working copy when a branch is
234
+ * opened and its clone turns out to be of another repository: given the
235
+ * workspace id and the move, it runs the move. The composition root
236
+ * passes what the startup phase does around its own set-asides: the
237
+ * commit worker held still, the copy's queued commits held back for a
238
+ * person, the locks on its branch dropped. If it throws, the copy stays
239
+ * where it was and the branch is refused with that reason. Absent, the
240
+ * move runs as it is.
241
+ */
242
+ aroundSetAside = (_workspaceId, move) => move()) {
217
243
  this.workspacesRoot = workspacesRoot;
218
244
  this.kb = kb;
219
245
  this.disk = disk;
220
246
  this.gitRunner = gitRunner;
247
+ this.setAsideRoot = setAsideRoot;
248
+ this.aroundSetAside = aroundSetAside;
221
249
  this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
222
250
  }
251
+ /**
252
+ * The set-asides in flight, one per branch. Two callers opening the same
253
+ * branch both find the clone of another repository; the second waits for
254
+ * the first's move instead of trying to move a directory that has gone.
255
+ */
256
+ inFlightSetAsides = new Map();
223
257
  get kbDirName() {
224
258
  return this.kb.kbDirName;
225
259
  }
@@ -369,6 +403,19 @@ export class WorkspaceService {
369
403
  * a conventional default branch isn't on the remote) just to run a global op.
370
404
  * Scans disk rather than the in-memory `branchDirs` map so clones that
371
405
  * survived a process restart are still found.
406
+ *
407
+ * INTERCHANGEABLE ONLY ONCE IT CAN REACH THE REMOTE. The clone handed back
408
+ * is one this process may never have opened, and opening a branch is the
409
+ * only other place a clone's credential helper is brought into line with the
410
+ * deployment's (`getOrCreateForBranch`). A clone made before the helper was
411
+ * persisted, or before a token was configured, has none — and it is exactly
412
+ * the kind that sorts first on disk, because it is old. Every repo-global
413
+ * operation then ran its fetches in a working copy that could not
414
+ * authenticate: the fetch failed, quietly, and each change request whose
415
+ * branch that clone had never seen was reported as `unknown branch`, list
416
+ * after list, while the branches sat on the remote. So the helper is stamped
417
+ * here too, before the clone is offered. A no-op for a clone already stamped
418
+ * in this process, and it never throws (see `stampCredentialHelper`).
372
419
  */
373
420
  async findAnyWorkspaceId() {
374
421
  let entries;
@@ -381,13 +428,16 @@ export class WorkspaceService {
381
428
  for (const entry of entries) {
382
429
  if (!entry.isDirectory())
383
430
  continue;
431
+ const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
384
432
  try {
385
- await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
386
- return entry.name;
433
+ await fs.access(path.join(repoDir, '.git'));
387
434
  }
388
435
  catch {
389
436
  // No `.git` — half-built or unrelated dir; keep looking.
437
+ continue;
390
438
  }
439
+ await this.refreshCredentialHelperIfStale(branchForWorkspaceId(entry.name), repoDir);
440
+ return entry.name;
391
441
  }
392
442
  return null;
393
443
  }
@@ -526,22 +576,111 @@ export class WorkspaceService {
526
576
  }
527
577
  // Check disk — the clone may have been bootstrapped by a previous
528
578
  // process and survived a restart.
529
- try {
530
- await fs.access(path.join(repoDir, '.git'));
531
- // Migration for clones already on disk: re-stamp the tracking config the
532
- // first time this process opens them, so a clone whose config drifted
533
- // (duplicate fetch refspec / merge ref) is repaired before anything
534
- // pulls it. Once per branch per process — the cached paths above return
535
- // before reaching here.
536
- await this.normalizeCloneConfig(repoDir, branch);
537
- this.registerBranchDir(branch, workspaceDir);
538
- return this.buildWorkspaceInfo(branch, workspaceDir);
579
+ const onDisk = await fs.access(path.join(repoDir, '.git')).then(() => true, () => false);
580
+ // A clone fetches and pushes through the address stored in its OWN
581
+ // `remote.origin.url`, which nothing rewrites when the configured
582
+ // repository changes. Adopting one of the repository that was replaced is
583
+ // how every later fetch answers "repository not found" — so it is set
584
+ // aside here and cloned fresh below, the same rule, and the same
585
+ // destination, as the KB startup phase applies to the whole workspaces
586
+ // root.
587
+ //
588
+ // SET ASIDE, never deleted. The phase sweeps only AFTER it has reached the
589
+ // configured remote, so that a typo in the address cannot cost anyone
590
+ // their work. This path has reached nothing: it is a branch being opened,
591
+ // and it runs in exactly the states where the address is stored but
592
+ // unproven — a gated boot, a break-glass start, a save that changed the
593
+ // address before the phase got to it. Deleting on a string difference
594
+ // there would throw away commits that exist nowhere else because somebody
595
+ // mistyped a host name.
596
+ //
597
+ // OUTSIDE the adoption `try` below, deliberately: a clone that cannot be
598
+ // moved must fail this call with that reason. Swallowed into "bootstrap
599
+ // below", it would be found again by the clone step, which sees a
600
+ // directory with a `.git` in it, calls that already-cloned, and hands back
601
+ // the very working copy this refused to adopt.
602
+ // Counted before the disk is looked at: the startup phase may take this
603
+ // very copy away while the checks below are awaited (`forgetClone`), and a
604
+ // copy that went must not be registered as the branch's.
605
+ const discardsAtAdoption = this.discardsOf(id);
606
+ const setAsideInFlight = this.inFlightSetAsides.get(branch);
607
+ if (setAsideInFlight) {
608
+ // Someone else is moving it. Wait, then clone below; a move that failed
609
+ // refuses this caller with the same reason.
610
+ await setAsideInFlight;
611
+ }
612
+ else if (onDisk && (await this.isCloneOfAnotherRepository(repoDir))) {
613
+ // Asked again: another caller may have started the move while the
614
+ // address was being read.
615
+ const startedMeanwhile = this.inFlightSetAsides.get(branch);
616
+ if (startedMeanwhile) {
617
+ await startedMeanwhile;
618
+ }
619
+ else if (this.discardsOf(id) !== discardsAtAdoption) {
620
+ // The copy whose address this call read is no longer there: a move
621
+ // began AND ended while it was being read. An in-flight entry is
622
+ // cleared the moment its move ends, so a caller that comes back after
623
+ // that finds nobody to wait for — and its answer is about a copy that
624
+ // has already been set aside. Acting on it would set aside whatever is
625
+ // at this path NOW, which is the fresh clone the first caller is in
626
+ // the middle of making. Nothing is left to move; the bootstrap below
627
+ // waits for that clone.
628
+ }
629
+ else {
630
+ const kept = path.join(setAsideRootFor(this.workspacesRoot, this.setAsideRoot), setAsideStamp(), id);
631
+ // What surrounds the move may wait, and the startup phase may set
632
+ // this very copy aside meanwhile. Then there is nothing left to move:
633
+ // the phase did it, and said so.
634
+ let moved = false;
635
+ const moving = this.aroundSetAside(id, async () => {
636
+ if (this.discardsOf(id) !== discardsAtAdoption)
637
+ return;
638
+ await setAsideClone(repoDir, kept);
639
+ moved = true;
640
+ // Said the way the startup phase says it: the copy at this path was
641
+ // taken away. That is what the check above reads, so every caller
642
+ // that looked at the old copy before this line knows its answer is
643
+ // stale, however late it comes back.
644
+ this.forgetClone(id);
645
+ }).finally(() => {
646
+ this.inFlightSetAsides.delete(branch);
647
+ });
648
+ this.inFlightSetAsides.set(branch, moving);
649
+ await moving;
650
+ if (moved) {
651
+ log.warn(`the "${branch}" working copy was a clone of another repository. Set aside at ${kept}; nothing was ` +
652
+ 'deleted, and it is being cloned fresh from the configured one. Work that was never pushed is in ' +
653
+ 'that folder: `git log` there shows it.');
654
+ }
655
+ }
539
656
  }
540
- catch {
541
- // Not on disk — bootstrap below.
657
+ else if (onDisk) {
658
+ try {
659
+ // Migration for clones already on disk: re-stamp the tracking config
660
+ // the first time this process opens them, so a clone whose config
661
+ // drifted (duplicate fetch refspec / merge ref) is repaired before
662
+ // anything pulls it. Once per branch per process — the cached paths
663
+ // above return before reaching here.
664
+ await this.normalizeCloneConfig(repoDir, branch);
665
+ // Taken away meanwhile: nothing to adopt, clone below.
666
+ if (this.discardsOf(id) !== discardsAtAdoption)
667
+ throw new Error('the working copy was set aside');
668
+ this.registerBranchDir(branch, workspaceDir);
669
+ return this.buildWorkspaceInfo(branch, workspaceDir);
670
+ }
671
+ catch {
672
+ // No longer worth adopting — bootstrap below.
673
+ }
542
674
  }
543
675
  // Single-flight bootstrap. Concurrent callers for the same branch
544
676
  // share the first one's promise instead of stacking clones.
677
+ //
678
+ // Recorded before any of it: a clone takes seconds, and the save that
679
+ // replaces the repository can land in the middle of one. Whatever this
680
+ // bootstrap produces is about the repository configured NOW, so if the
681
+ // working copy is taken away while it runs (`forgetClone`) the result must
682
+ // not be published — see `adoptBootstrapped`.
683
+ const discardsAtBootstrap = this.discardsOf(workspaceIdForBranch(branch));
545
684
  let resolveBootstrap;
546
685
  let rejectBootstrap;
547
686
  const bootstrap = new Promise((res, rej) => {
@@ -552,7 +691,7 @@ export class WorkspaceService {
552
691
  const existingBootstrap = this.inFlightBootstraps.get(branch);
553
692
  if (existingBootstrap) {
554
693
  await existingBootstrap;
555
- this.registerBranchDir(branch, workspaceDir);
694
+ this.adoptBootstrapped(branch, workspaceDir, discardsAtBootstrap);
556
695
  return this.buildWorkspaceInfo(branch, workspaceDir);
557
696
  }
558
697
  this.inFlightBootstraps.set(branch, bootstrap);
@@ -566,7 +705,7 @@ export class WorkspaceService {
566
705
  // naturally; seeding the remote is the KB startup phase's job, at boot.
567
706
  createdWorkspaceDir = await fs.mkdir(workspaceDir, { recursive: true });
568
707
  await this.cloneProcessMapForBranch(workspaceDir, branch);
569
- this.registerBranchDir(branch, workspaceDir);
708
+ this.adoptBootstrapped(branch, workspaceDir, discardsAtBootstrap);
570
709
  resolveBootstrap();
571
710
  }
572
711
  catch (err) {
@@ -611,6 +750,30 @@ export class WorkspaceService {
611
750
  kbDirName: this.kbDirName,
612
751
  };
613
752
  }
753
+ /**
754
+ * Publish a finished bootstrap's working copy to the branch→directory cache
755
+ * — unless the copy was taken out from under it while it ran.
756
+ *
757
+ * The save that replaces the knowledge-base repository sets every working
758
+ * copy aside and clears this cache (`forgetClone`), and a clone takes
759
+ * seconds: a bootstrap already in flight when that happens finishes
760
+ * afterwards, and a plain `registerBranchDir` would put the path back. The
761
+ * directory is gone (set aside), so every reader from then on gets an
762
+ * ENOENT off the fast path and nothing ever re-clones it — a cache entry
763
+ * is exactly what stops that.
764
+ *
765
+ * Refused rather than retried here: the address this bootstrap cloned from
766
+ * is not the one configured now, so there is nothing to salvage. The caller
767
+ * is told, and opening the branch again clones it from the new address.
768
+ */
769
+ adoptBootstrapped(branch, workspaceDir, discardsAtStart) {
770
+ if (this.discardsOf(workspaceIdForBranch(branch)) !== discardsAtStart) {
771
+ this.branchDirs.delete(branch);
772
+ throw new Error(`The knowledge-base repository was replaced while the "${branch}" working copy was being created, so ` +
773
+ 'that copy was set aside instead of being used. Open it again to get one of the new repository.');
774
+ }
775
+ this.registerBranchDir(branch, workspaceDir);
776
+ }
614
777
  /**
615
778
  * Await the in-flight bootstrap for a branch (if any). On reject —
616
779
  * meaning the clone rolled back — drop the branch→dir mapping so the
@@ -636,6 +799,94 @@ export class WorkspaceService {
636
799
  isBootstrapInFlight(branch) {
637
800
  return this.inFlightBootstraps.has(branch);
638
801
  }
802
+ /**
803
+ * Whether the clone at `repoDir` belongs to a repository OTHER than the
804
+ * configured one.
805
+ *
806
+ * Errs towards NO — "could not tell" must never delete someone's work:
807
+ * an address that cannot be read (no `origin`, a git that would not run)
808
+ * leaves the clone alone, and only two addresses that are read and differ
809
+ * in more than their spelling count as different repositories (see
810
+ * `kb-fs/remote-url.ts`). An UNCONFIGURED address answers no as well: a
811
+ * process that does not know where the repository is has no business
812
+ * declaring a clone stale.
813
+ */
814
+ async isCloneOfAnotherRepository(repoDir) {
815
+ const configured = this.kbRepoUrl().trim();
816
+ if (configured === '')
817
+ return false;
818
+ let origin;
819
+ try {
820
+ const { stdout } = await this.gitRunner.run(repoDir, ['config', '--get', 'remote.origin.url']);
821
+ origin = stdout.trim();
822
+ }
823
+ catch {
824
+ return false;
825
+ }
826
+ if (origin === '' || sameRepository(origin, configured))
827
+ return false;
828
+ // Normalizing strips the userinfo but KEEPS the query string, and the
829
+ // knowledge-base address is one that may carry its credential there (a
830
+ // signed URL — see `urlQuerySecrets`). So the address is scrubbed on the
831
+ // way into the log, exactly as the startup phase scrubs its own.
832
+ log.warn(`the "${path.basename(path.dirname(repoDir))}" working copy is a clone of another repository ` +
833
+ `(${redactSecret(normalizeRepositoryAddress(origin), urlQuerySecrets(origin))}).`);
834
+ return true;
835
+ }
836
+ /**
837
+ * How many times each working copy has been taken out from under this
838
+ * process — see {@link forgetClone}. Deleting from the caches is not enough
839
+ * on a RUNNING server: work already in flight against the copy that went
840
+ * away finishes afterwards and writes its result back into those very
841
+ * caches. So every such reader records the count it started with and checks
842
+ * it before publishing: a count that moved means what it learned is about a
843
+ * working copy that no longer exists, and is dropped.
844
+ *
845
+ * Keyed by workspace id, which is what the phase announces and what both
846
+ * readers can derive (`workspaceIdForBranch`).
847
+ */
848
+ discardCount = new Map();
849
+ /**
850
+ * Both readers and `forgetClone` go through this, so a caller's spelling of
851
+ * a workspace id can never make them disagree about which working copy is
852
+ * meant: the id is taken back to its branch and spelled afresh, which is the
853
+ * one form the phase announces.
854
+ */
855
+ discardKey(workspaceId) {
856
+ return workspaceIdForBranch(branchForWorkspaceId(workspaceId));
857
+ }
858
+ discardsOf(workspaceId) {
859
+ return this.discardCount.get(this.discardKey(workspaceId)) ?? 0;
860
+ }
861
+ /**
862
+ * Forget everything this process remembers about a working copy that was
863
+ * taken away underneath it — the KB startup phase setting aside a clone of a
864
+ * repository that was replaced.
865
+ *
866
+ * At boot there is nothing to forget; this is for the save that changes the
867
+ * address on a RUNNING server, where `branchDirs` would otherwise keep
868
+ * answering with the path of a directory that is gone and no later caller
869
+ * would ever re-clone it. The branch NAME is deliberately left in
870
+ * `branchesEverHeardOf` — the branch was real, and that record outlives
871
+ * its clone.
872
+ */
873
+ forgetClone(workspaceId) {
874
+ const branch = branchForWorkspaceId(workspaceId);
875
+ const workspaceDir = path.join(this.workspacesRoot, workspaceId);
876
+ const repoDir = path.join(workspaceDir, this.kbDirName);
877
+ this.discardCount.set(this.discardKey(workspaceId), this.discardsOf(workspaceId) + 1);
878
+ this.branchDirs.delete(branch);
879
+ this.lastFetchAt.delete(repoDir);
880
+ this.lastFetchOk.delete(repoDir);
881
+ // Dropped as well as un-stamped: the promise still running belongs to the
882
+ // working copy that went away, so a caller that joined it would be told a
883
+ // fetch of a directory that is gone refreshed the one that replaced it.
884
+ // The count above is what stops that promise re-stamping the TTL when it
885
+ // finally settles.
886
+ this.inFlightFetches.delete(repoDir);
887
+ // Keyed by branch, not by directory — see `stampCredentialHelper`.
888
+ this.stampedCredentialFingerprint.delete(branch);
889
+ }
639
890
  /**
640
891
  * Bring an adopted clone's config back to what a fresh clone would carry.
641
892
  *
@@ -1154,14 +1405,25 @@ export class WorkspaceService {
1154
1405
  // This driver runs outside the git layer's per-workspace mutex, so it may
1155
1406
  // only run the safe implicit-fetch shape — see `SAFE_IMPLICIT_FETCH_ARGS`
1156
1407
  // in `kb-fs/clone-config.ts` for the full rationale.
1408
+ // What this fetch is ABOUT: the working copy in that directory as it is
1409
+ // now. Should the repository be replaced while the fetch runs, the copy is
1410
+ // set aside and a fresh one is cloned to the SAME path — so stamping the
1411
+ // result afterwards would hand the new clone a freshness it never earned
1412
+ // (or a failure that was the old one's), and the next reader would skip
1413
+ // refreshing it for the whole TTL.
1414
+ const discardsAtFetch = this.discardsOf(workspaceId);
1415
+ const stillTheSameClone = () => this.discardsOf(workspaceId) === discardsAtFetch;
1157
1416
  const promise = this.gitRunner
1158
1417
  .run(repoDir, [...SAFE_IMPLICIT_FETCH_ARGS])
1159
1418
  .then(() => {
1419
+ if (!stillTheSameClone())
1420
+ return;
1160
1421
  this.lastFetchAt.set(repoDir, Date.now());
1161
1422
  this.lastFetchOk.set(repoDir, true);
1162
1423
  })
1163
1424
  .catch((err) => {
1164
- this.lastFetchOk.set(repoDir, false);
1425
+ if (stillTheSameClone())
1426
+ this.lastFetchOk.set(repoDir, false);
1165
1427
  log.warn('git fetch origin failed:', { detail: redactError(err, this.gitRunner.credentials.token()) });
1166
1428
  })
1167
1429
  .finally(() => {
@@ -1544,10 +1806,10 @@ export class WorkspaceService {
1544
1806
  */
1545
1807
  async unzipFile(workspaceId, zipWsPath, destDirWsPath,
1546
1808
  /**
1547
- * Per-extracted-path write guard (ontology-session boundary). Called with
1548
- * each entry's workspace-relative target before it is written; if it throws,
1549
- * the entry is skipped (with the thrown message as the reason) rather than
1550
- * aborting the whole extraction. Omitted by non-agent / human callers.
1809
+ * Per-extracted-path write guard. Called with each entry's
1810
+ * workspace-relative target before it is written; if it throws, the entry
1811
+ * is skipped (with the thrown message as the reason) rather than aborting
1812
+ * the whole extraction. Omitted by non-agent / human callers.
1551
1813
  */
1552
1814
  guardWrite) {
1553
1815
  const { workspaceDir, relativePath: zipRelativePath, absolutePath: zipAbsolute } = await this.resolveInsideRepo(workspaceId, zipWsPath);
@@ -1616,37 +1878,20 @@ export class WorkspaceService {
1616
1878
  continue;
1617
1879
  }
1618
1880
  processedCount++;
1619
- const rawName = entry.entryName.replace(/\\/g, '/');
1620
- if (rawName.startsWith('__MACOSX/') ||
1621
- rawName === '__MACOSX' ||
1622
- rawName.endsWith('/.DS_Store') ||
1623
- rawName === '.DS_Store' ||
1624
- /(^|\/)\._/.test(rawName)) {
1881
+ const rawName = zipEntryName(entry.entryName);
1882
+ if (isZipNoiseEntry(rawName))
1625
1883
  continue;
1626
- }
1627
- if (!rawName || rawName.startsWith('/') || /(^|\/)\.\.($|\/)/.test(rawName)) {
1628
- skipped.push({ path: rawName || '(empty)', reason: 'Invalid path' });
1629
- continue;
1630
- }
1631
- // An archive never writes into the git folder, whatever the entry's spelling.
1632
- if (hasGitInternalsSegment(rawName)) {
1633
- skipped.push({ path: rawName, reason: new GitInternalsError().message });
1884
+ // What an entry's NAME may be — invalid path, the git folder in any
1885
+ // spelling, a segment no filesystem keeps intact — is `zip-entry-rules`,
1886
+ // so `apply_file_upload` judges the same names by the same rules rather
1887
+ // than by a second copy of them.
1888
+ const nameRefusal = zipEntryNameRefusal(rawName);
1889
+ if (nameRefusal !== null) {
1890
+ skipped.push({ path: rawName || '(empty)', reason: nameRefusal });
1634
1891
  continue;
1635
1892
  }
1636
1893
  const trimmed = rawName.replace(/\/+$/, '');
1637
- const segments = trimmed.split('/').filter((s) => s.length > 0);
1638
- let invalidReason = null;
1639
- for (const segment of segments) {
1640
- const reason = validateFilename(segment);
1641
- if (reason) {
1642
- invalidReason = reason;
1643
- break;
1644
- }
1645
- }
1646
- if (invalidReason) {
1647
- skipped.push({ path: rawName, reason: invalidReason });
1648
- continue;
1649
- }
1894
+ const segments = zipEntrySegments(rawName);
1650
1895
  const targetAbsolute = path.resolve(destAbsolute, ...segments);
1651
1896
  const destRoot = path.resolve(destAbsolute);
1652
1897
  if (targetAbsolute !== destRoot && !targetAbsolute.startsWith(destRoot + path.sep)) {
@@ -1656,10 +1901,10 @@ export class WorkspaceService {
1656
1901
  const relForReport = path
1657
1902
  .relative(workspaceDir, targetAbsolute)
1658
1903
  .replace(/\\/g, '/');
1659
- // Ontology-session boundary: a write-blocked (or cross-ontology) entry is
1660
- // skipped, not extracted, so an archive can't be a write path around it.
1661
- // Runs before any filesystem side effect — including directory creation —
1662
- // so a blocked entry leaves nothing behind on disk.
1904
+ // An entry the write guard refuses is skipped, not extracted, so an
1905
+ // archive can't be a way around whatever the guard enforces. Runs before
1906
+ // any filesystem side effect — including directory creation — so a
1907
+ // refused entry leaves nothing behind on disk.
1663
1908
  const allowEntry = async () => {
1664
1909
  if (!guardWrite)
1665
1910
  return true;
@@ -1668,7 +1913,7 @@ export class WorkspaceService {
1668
1913
  return true;
1669
1914
  }
1670
1915
  catch (err) {
1671
- skipped.push({ path: rawName, reason: err instanceof Error ? err.message : 'Blocked by the ontology-session boundary' });
1916
+ skipped.push({ path: rawName, reason: err instanceof Error ? err.message : 'Refused before it was written' });
1672
1917
  return false;
1673
1918
  }
1674
1919
  };
@@ -1725,14 +1970,27 @@ export class WorkspaceService {
1725
1970
  });
1726
1971
  continue;
1727
1972
  }
1728
- const data = entry.getData();
1729
- if (data.byteLength > UNZIP_MAX_ENTRY_BYTES) {
1973
+ // Read through the one bounded reader (`readZipEntry`), never with the
1974
+ // declared size above as the only bound: an entry that declares ZERO is
1975
+ // inflated with no cap at all by the archive reader, so a few kilobytes
1976
+ // could expand to whatever memory there is before the checks below ran.
1977
+ // Capped at what is left of both limits, and a read that fails is this
1978
+ // entry's refusal rather than the whole archive's.
1979
+ const read = readZipEntry(entry, Math.min(UNZIP_MAX_ENTRY_BYTES, UNZIP_MAX_TOTAL_BYTES - totalUncompressed));
1980
+ if (!read.ok && read.reason === 'unreadable') {
1981
+ skipped.push({ path: rawName, reason: `Entry could not be read: ${read.detail}` });
1982
+ continue;
1983
+ }
1984
+ if (!read.ok) {
1730
1985
  skipped.push({
1731
1986
  path: rawName,
1732
- reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
1987
+ reason: totalUncompressed + UNZIP_MAX_ENTRY_BYTES > UNZIP_MAX_TOTAL_BYTES
1988
+ ? `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`
1989
+ : `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
1733
1990
  });
1734
1991
  continue;
1735
1992
  }
1993
+ const data = read.data;
1736
1994
  if (totalUncompressed + data.byteLength > UNZIP_MAX_TOTAL_BYTES) {
1737
1995
  skipped.push({
1738
1996
  path: rawName,