@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
@@ -8,7 +8,6 @@ import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '
8
8
  import { AsyncLocalStorage } from 'node:async_hooks';
9
9
  import {
10
10
  validateRelativePath,
11
- validateFilename,
12
11
  FOLDER_PLACEHOLDER,
13
12
  isFolderPlaceholder,
14
13
  entryExistsMessage,
@@ -26,6 +25,7 @@ import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
26
25
  import { assertWithinDirectory } from '../../shared/path-containment.js';
27
26
  import { assertRepoRootNameFree, normalizeWorkspacePath } from '../kb-fs/repo-path.js';
28
27
  import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
28
+ import { isZipNoiseEntry, readZipEntry, zipEntryName, zipEntryNameRefusal, zipEntrySegments } from './zip-entry-rules.js';
29
29
  import {
30
30
  GitInternalsError,
31
31
  PathNotFoundError,
@@ -41,6 +41,9 @@ import {
41
41
  isMissingRemoteBranchFailure,
42
42
  } from '../../shared/domain-errors.js';
43
43
  import { assertValidBranchName } from '../kb-fs/branch-name.js';
44
+ import { normalizeRepositoryAddress, sameRepository } from '../kb-fs/remote-url.js';
45
+ import { redactSecret, urlQuerySecrets } from '../../shared/redact-secret.js';
46
+ import { setAsideClone, setAsideRootFor, setAsideStamp } from './set-aside-clone.js';
44
47
  import {
45
48
  cloneCredentialArgs,
46
49
  cloneCredentialConfigArgs,
@@ -307,10 +310,40 @@ export class WorkspaceService implements IWorkspaceService {
307
310
  * constructed service.
308
311
  */
309
312
  private readonly gitRunner: IGitRunner = new NodeGitRunner(),
313
+ /**
314
+ * Where a working copy of a repository that is no longer the configured
315
+ * one is kept. THE SAME VALUE THE KB STARTUP PHASE IS GIVEN — what the two
316
+ * of them move must end up in one place, and omitting it here falls back to
317
+ * a directory that does not survive a deployment's next recreate.
318
+ *
319
+ * Why, in one place: `setAsideRootFor` in `set-aside-clone.ts`.
320
+ */
321
+ private readonly setAsideRoot?: string,
322
+ /**
323
+ * What surrounds the setting aside of ONE working copy when a branch is
324
+ * opened and its clone turns out to be of another repository: given the
325
+ * workspace id and the move, it runs the move. The composition root
326
+ * passes what the startup phase does around its own set-asides: the
327
+ * commit worker held still, the copy's queued commits held back for a
328
+ * person, the locks on its branch dropped. If it throws, the copy stays
329
+ * where it was and the branch is refused with that reason. Absent, the
330
+ * move runs as it is.
331
+ */
332
+ private readonly aroundSetAside: (workspaceId: string, move: () => Promise<void>) => Promise<void> = (
333
+ _workspaceId,
334
+ move,
335
+ ) => move(),
310
336
  ) {
311
337
  this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
312
338
  }
313
339
 
340
+ /**
341
+ * The set-asides in flight, one per branch. Two callers opening the same
342
+ * branch both find the clone of another repository; the second waits for
343
+ * the first's move instead of trying to move a directory that has gone.
344
+ */
345
+ private readonly inFlightSetAsides = new Map<string, Promise<void>>();
346
+
314
347
  private get kbDirName(): string {
315
348
  return this.kb.kbDirName;
316
349
  }
@@ -462,6 +495,19 @@ export class WorkspaceService implements IWorkspaceService {
462
495
  * a conventional default branch isn't on the remote) just to run a global op.
463
496
  * Scans disk rather than the in-memory `branchDirs` map so clones that
464
497
  * survived a process restart are still found.
498
+ *
499
+ * INTERCHANGEABLE ONLY ONCE IT CAN REACH THE REMOTE. The clone handed back
500
+ * is one this process may never have opened, and opening a branch is the
501
+ * only other place a clone's credential helper is brought into line with the
502
+ * deployment's (`getOrCreateForBranch`). A clone made before the helper was
503
+ * persisted, or before a token was configured, has none — and it is exactly
504
+ * the kind that sorts first on disk, because it is old. Every repo-global
505
+ * operation then ran its fetches in a working copy that could not
506
+ * authenticate: the fetch failed, quietly, and each change request whose
507
+ * branch that clone had never seen was reported as `unknown branch`, list
508
+ * after list, while the branches sat on the remote. So the helper is stamped
509
+ * here too, before the clone is offered. A no-op for a clone already stamped
510
+ * in this process, and it never throws (see `stampCredentialHelper`).
465
511
  */
466
512
  async findAnyWorkspaceId(): Promise<string | null> {
467
513
  let entries: Array<{ name: string; isDirectory: () => boolean }>;
@@ -472,12 +518,15 @@ export class WorkspaceService implements IWorkspaceService {
472
518
  }
473
519
  for (const entry of entries) {
474
520
  if (!entry.isDirectory()) continue;
521
+ const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
475
522
  try {
476
- await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
477
- return entry.name;
523
+ await fs.access(path.join(repoDir, '.git'));
478
524
  } catch {
479
525
  // No `.git` — half-built or unrelated dir; keep looking.
526
+ continue;
480
527
  }
528
+ await this.refreshCredentialHelperIfStale(branchForWorkspaceId(entry.name), repoDir);
529
+ return entry.name;
481
530
  }
482
531
  return null;
483
532
  }
@@ -617,22 +666,107 @@ export class WorkspaceService implements IWorkspaceService {
617
666
 
618
667
  // Check disk — the clone may have been bootstrapped by a previous
619
668
  // process and survived a restart.
620
- try {
621
- await fs.access(path.join(repoDir, '.git'));
622
- // Migration for clones already on disk: re-stamp the tracking config the
623
- // first time this process opens them, so a clone whose config drifted
624
- // (duplicate fetch refspec / merge ref) is repaired before anything
625
- // pulls it. Once per branch per process — the cached paths above return
626
- // before reaching here.
627
- await this.normalizeCloneConfig(repoDir, branch);
628
- this.registerBranchDir(branch, workspaceDir);
629
- return this.buildWorkspaceInfo(branch, workspaceDir);
630
- } catch {
631
- // Not on disk — bootstrap below.
669
+ const onDisk = await fs.access(path.join(repoDir, '.git')).then(() => true, () => false);
670
+ // A clone fetches and pushes through the address stored in its OWN
671
+ // `remote.origin.url`, which nothing rewrites when the configured
672
+ // repository changes. Adopting one of the repository that was replaced is
673
+ // how every later fetch answers "repository not found" — so it is set
674
+ // aside here and cloned fresh below, the same rule, and the same
675
+ // destination, as the KB startup phase applies to the whole workspaces
676
+ // root.
677
+ //
678
+ // SET ASIDE, never deleted. The phase sweeps only AFTER it has reached the
679
+ // configured remote, so that a typo in the address cannot cost anyone
680
+ // their work. This path has reached nothing: it is a branch being opened,
681
+ // and it runs in exactly the states where the address is stored but
682
+ // unproven — a gated boot, a break-glass start, a save that changed the
683
+ // address before the phase got to it. Deleting on a string difference
684
+ // there would throw away commits that exist nowhere else because somebody
685
+ // mistyped a host name.
686
+ //
687
+ // OUTSIDE the adoption `try` below, deliberately: a clone that cannot be
688
+ // moved must fail this call with that reason. Swallowed into "bootstrap
689
+ // below", it would be found again by the clone step, which sees a
690
+ // directory with a `.git` in it, calls that already-cloned, and hands back
691
+ // the very working copy this refused to adopt.
692
+ // Counted before the disk is looked at: the startup phase may take this
693
+ // very copy away while the checks below are awaited (`forgetClone`), and a
694
+ // copy that went must not be registered as the branch's.
695
+ const discardsAtAdoption = this.discardsOf(id);
696
+ const setAsideInFlight = this.inFlightSetAsides.get(branch);
697
+ if (setAsideInFlight) {
698
+ // Someone else is moving it. Wait, then clone below; a move that failed
699
+ // refuses this caller with the same reason.
700
+ await setAsideInFlight;
701
+ } else if (onDisk && (await this.isCloneOfAnotherRepository(repoDir))) {
702
+ // Asked again: another caller may have started the move while the
703
+ // address was being read.
704
+ const startedMeanwhile = this.inFlightSetAsides.get(branch);
705
+ if (startedMeanwhile) {
706
+ await startedMeanwhile;
707
+ } else if (this.discardsOf(id) !== discardsAtAdoption) {
708
+ // The copy whose address this call read is no longer there: a move
709
+ // began AND ended while it was being read. An in-flight entry is
710
+ // cleared the moment its move ends, so a caller that comes back after
711
+ // that finds nobody to wait for — and its answer is about a copy that
712
+ // has already been set aside. Acting on it would set aside whatever is
713
+ // at this path NOW, which is the fresh clone the first caller is in
714
+ // the middle of making. Nothing is left to move; the bootstrap below
715
+ // waits for that clone.
716
+ } else {
717
+ const kept = path.join(setAsideRootFor(this.workspacesRoot, this.setAsideRoot), setAsideStamp(), id);
718
+ // What surrounds the move may wait, and the startup phase may set
719
+ // this very copy aside meanwhile. Then there is nothing left to move:
720
+ // the phase did it, and said so.
721
+ let moved = false;
722
+ const moving = this.aroundSetAside(id, async () => {
723
+ if (this.discardsOf(id) !== discardsAtAdoption) return;
724
+ await setAsideClone(repoDir, kept);
725
+ moved = true;
726
+ // Said the way the startup phase says it: the copy at this path was
727
+ // taken away. That is what the check above reads, so every caller
728
+ // that looked at the old copy before this line knows its answer is
729
+ // stale, however late it comes back.
730
+ this.forgetClone(id);
731
+ }).finally(() => {
732
+ this.inFlightSetAsides.delete(branch);
733
+ });
734
+ this.inFlightSetAsides.set(branch, moving);
735
+ await moving;
736
+ if (moved) {
737
+ log.warn(
738
+ `the "${branch}" working copy was a clone of another repository. Set aside at ${kept}; nothing was ` +
739
+ 'deleted, and it is being cloned fresh from the configured one. Work that was never pushed is in ' +
740
+ 'that folder: `git log` there shows it.',
741
+ );
742
+ }
743
+ }
744
+ } else if (onDisk) {
745
+ try {
746
+ // Migration for clones already on disk: re-stamp the tracking config
747
+ // the first time this process opens them, so a clone whose config
748
+ // drifted (duplicate fetch refspec / merge ref) is repaired before
749
+ // anything pulls it. Once per branch per process — the cached paths
750
+ // above return before reaching here.
751
+ await this.normalizeCloneConfig(repoDir, branch);
752
+ // Taken away meanwhile: nothing to adopt, clone below.
753
+ if (this.discardsOf(id) !== discardsAtAdoption) throw new Error('the working copy was set aside');
754
+ this.registerBranchDir(branch, workspaceDir);
755
+ return this.buildWorkspaceInfo(branch, workspaceDir);
756
+ } catch {
757
+ // No longer worth adopting — bootstrap below.
758
+ }
632
759
  }
633
760
 
634
761
  // Single-flight bootstrap. Concurrent callers for the same branch
635
762
  // share the first one's promise instead of stacking clones.
763
+ //
764
+ // Recorded before any of it: a clone takes seconds, and the save that
765
+ // replaces the repository can land in the middle of one. Whatever this
766
+ // bootstrap produces is about the repository configured NOW, so if the
767
+ // working copy is taken away while it runs (`forgetClone`) the result must
768
+ // not be published — see `adoptBootstrapped`.
769
+ const discardsAtBootstrap = this.discardsOf(workspaceIdForBranch(branch));
636
770
  let resolveBootstrap!: () => void;
637
771
  let rejectBootstrap!: (err: unknown) => void;
638
772
  const bootstrap = new Promise<void>((res, rej) => {
@@ -644,7 +778,7 @@ export class WorkspaceService implements IWorkspaceService {
644
778
  const existingBootstrap = this.inFlightBootstraps.get(branch);
645
779
  if (existingBootstrap) {
646
780
  await existingBootstrap;
647
- this.registerBranchDir(branch, workspaceDir);
781
+ this.adoptBootstrapped(branch, workspaceDir, discardsAtBootstrap);
648
782
  return this.buildWorkspaceInfo(branch, workspaceDir);
649
783
  }
650
784
  this.inFlightBootstraps.set(branch, bootstrap);
@@ -659,7 +793,7 @@ export class WorkspaceService implements IWorkspaceService {
659
793
  // naturally; seeding the remote is the KB startup phase's job, at boot.
660
794
  createdWorkspaceDir = await fs.mkdir(workspaceDir, { recursive: true });
661
795
  await this.cloneProcessMapForBranch(workspaceDir, branch);
662
- this.registerBranchDir(branch, workspaceDir);
796
+ this.adoptBootstrapped(branch, workspaceDir, discardsAtBootstrap);
663
797
  resolveBootstrap();
664
798
  } catch (err) {
665
799
  this.branchDirs.delete(branch);
@@ -706,6 +840,33 @@ export class WorkspaceService implements IWorkspaceService {
706
840
  };
707
841
  }
708
842
 
843
+ /**
844
+ * Publish a finished bootstrap's working copy to the branch→directory cache
845
+ * — unless the copy was taken out from under it while it ran.
846
+ *
847
+ * The save that replaces the knowledge-base repository sets every working
848
+ * copy aside and clears this cache (`forgetClone`), and a clone takes
849
+ * seconds: a bootstrap already in flight when that happens finishes
850
+ * afterwards, and a plain `registerBranchDir` would put the path back. The
851
+ * directory is gone (set aside), so every reader from then on gets an
852
+ * ENOENT off the fast path and nothing ever re-clones it — a cache entry
853
+ * is exactly what stops that.
854
+ *
855
+ * Refused rather than retried here: the address this bootstrap cloned from
856
+ * is not the one configured now, so there is nothing to salvage. The caller
857
+ * is told, and opening the branch again clones it from the new address.
858
+ */
859
+ private adoptBootstrapped(branch: string, workspaceDir: string, discardsAtStart: number): void {
860
+ if (this.discardsOf(workspaceIdForBranch(branch)) !== discardsAtStart) {
861
+ this.branchDirs.delete(branch);
862
+ throw new Error(
863
+ `The knowledge-base repository was replaced while the "${branch}" working copy was being created, so ` +
864
+ 'that copy was set aside instead of being used. Open it again to get one of the new repository.',
865
+ );
866
+ }
867
+ this.registerBranchDir(branch, workspaceDir);
868
+ }
869
+
709
870
  /**
710
871
  * Await the in-flight bootstrap for a branch (if any). On reject —
711
872
  * meaning the clone rolled back — drop the branch→dir mapping so the
@@ -731,6 +892,98 @@ export class WorkspaceService implements IWorkspaceService {
731
892
  return this.inFlightBootstraps.has(branch);
732
893
  }
733
894
 
895
+ /**
896
+ * Whether the clone at `repoDir` belongs to a repository OTHER than the
897
+ * configured one.
898
+ *
899
+ * Errs towards NO — "could not tell" must never delete someone's work:
900
+ * an address that cannot be read (no `origin`, a git that would not run)
901
+ * leaves the clone alone, and only two addresses that are read and differ
902
+ * in more than their spelling count as different repositories (see
903
+ * `kb-fs/remote-url.ts`). An UNCONFIGURED address answers no as well: a
904
+ * process that does not know where the repository is has no business
905
+ * declaring a clone stale.
906
+ */
907
+ private async isCloneOfAnotherRepository(repoDir: string): Promise<boolean> {
908
+ const configured = this.kbRepoUrl().trim();
909
+ if (configured === '') return false;
910
+ let origin: string;
911
+ try {
912
+ const { stdout } = await this.gitRunner.run(repoDir, ['config', '--get', 'remote.origin.url']);
913
+ origin = stdout.trim();
914
+ } catch {
915
+ return false;
916
+ }
917
+ if (origin === '' || sameRepository(origin, configured)) return false;
918
+ // Normalizing strips the userinfo but KEEPS the query string, and the
919
+ // knowledge-base address is one that may carry its credential there (a
920
+ // signed URL — see `urlQuerySecrets`). So the address is scrubbed on the
921
+ // way into the log, exactly as the startup phase scrubs its own.
922
+ log.warn(
923
+ `the "${path.basename(path.dirname(repoDir))}" working copy is a clone of another repository ` +
924
+ `(${redactSecret(normalizeRepositoryAddress(origin), urlQuerySecrets(origin))}).`,
925
+ );
926
+ return true;
927
+ }
928
+
929
+ /**
930
+ * How many times each working copy has been taken out from under this
931
+ * process — see {@link forgetClone}. Deleting from the caches is not enough
932
+ * on a RUNNING server: work already in flight against the copy that went
933
+ * away finishes afterwards and writes its result back into those very
934
+ * caches. So every such reader records the count it started with and checks
935
+ * it before publishing: a count that moved means what it learned is about a
936
+ * working copy that no longer exists, and is dropped.
937
+ *
938
+ * Keyed by workspace id, which is what the phase announces and what both
939
+ * readers can derive (`workspaceIdForBranch`).
940
+ */
941
+ private readonly discardCount = new Map<string, number>();
942
+
943
+ /**
944
+ * Both readers and `forgetClone` go through this, so a caller's spelling of
945
+ * a workspace id can never make them disagree about which working copy is
946
+ * meant: the id is taken back to its branch and spelled afresh, which is the
947
+ * one form the phase announces.
948
+ */
949
+ private discardKey(workspaceId: string): string {
950
+ return workspaceIdForBranch(branchForWorkspaceId(workspaceId));
951
+ }
952
+
953
+ private discardsOf(workspaceId: string): number {
954
+ return this.discardCount.get(this.discardKey(workspaceId)) ?? 0;
955
+ }
956
+
957
+ /**
958
+ * Forget everything this process remembers about a working copy that was
959
+ * taken away underneath it — the KB startup phase setting aside a clone of a
960
+ * repository that was replaced.
961
+ *
962
+ * At boot there is nothing to forget; this is for the save that changes the
963
+ * address on a RUNNING server, where `branchDirs` would otherwise keep
964
+ * answering with the path of a directory that is gone and no later caller
965
+ * would ever re-clone it. The branch NAME is deliberately left in
966
+ * `branchesEverHeardOf` — the branch was real, and that record outlives
967
+ * its clone.
968
+ */
969
+ forgetClone(workspaceId: string): void {
970
+ const branch = branchForWorkspaceId(workspaceId);
971
+ const workspaceDir = path.join(this.workspacesRoot, workspaceId);
972
+ const repoDir = path.join(workspaceDir, this.kbDirName);
973
+ this.discardCount.set(this.discardKey(workspaceId), this.discardsOf(workspaceId) + 1);
974
+ this.branchDirs.delete(branch);
975
+ this.lastFetchAt.delete(repoDir);
976
+ this.lastFetchOk.delete(repoDir);
977
+ // Dropped as well as un-stamped: the promise still running belongs to the
978
+ // working copy that went away, so a caller that joined it would be told a
979
+ // fetch of a directory that is gone refreshed the one that replaced it.
980
+ // The count above is what stops that promise re-stamping the TTL when it
981
+ // finally settles.
982
+ this.inFlightFetches.delete(repoDir);
983
+ // Keyed by branch, not by directory — see `stampCredentialHelper`.
984
+ this.stampedCredentialFingerprint.delete(branch);
985
+ }
986
+
734
987
  /**
735
988
  * Bring an adopted clone's config back to what a fresh clone would carry.
736
989
  *
@@ -1266,14 +1519,23 @@ export class WorkspaceService implements IWorkspaceService {
1266
1519
  // This driver runs outside the git layer's per-workspace mutex, so it may
1267
1520
  // only run the safe implicit-fetch shape — see `SAFE_IMPLICIT_FETCH_ARGS`
1268
1521
  // in `kb-fs/clone-config.ts` for the full rationale.
1522
+ // What this fetch is ABOUT: the working copy in that directory as it is
1523
+ // now. Should the repository be replaced while the fetch runs, the copy is
1524
+ // set aside and a fresh one is cloned to the SAME path — so stamping the
1525
+ // result afterwards would hand the new clone a freshness it never earned
1526
+ // (or a failure that was the old one's), and the next reader would skip
1527
+ // refreshing it for the whole TTL.
1528
+ const discardsAtFetch = this.discardsOf(workspaceId);
1529
+ const stillTheSameClone = (): boolean => this.discardsOf(workspaceId) === discardsAtFetch;
1269
1530
  const promise = this.gitRunner
1270
1531
  .run(repoDir, [...SAFE_IMPLICIT_FETCH_ARGS])
1271
1532
  .then(() => {
1533
+ if (!stillTheSameClone()) return;
1272
1534
  this.lastFetchAt.set(repoDir, Date.now());
1273
1535
  this.lastFetchOk.set(repoDir, true);
1274
1536
  })
1275
1537
  .catch((err) => {
1276
- this.lastFetchOk.set(repoDir, false);
1538
+ if (stillTheSameClone()) this.lastFetchOk.set(repoDir, false);
1277
1539
  log.warn('git fetch origin failed:', { detail: redactError(err, this.gitRunner.credentials.token()) });
1278
1540
  })
1279
1541
  .finally(() => {
@@ -1689,10 +1951,10 @@ export class WorkspaceService implements IWorkspaceService {
1689
1951
  zipWsPath: string,
1690
1952
  destDirWsPath?: string,
1691
1953
  /**
1692
- * Per-extracted-path write guard (ontology-session boundary). Called with
1693
- * each entry's workspace-relative target before it is written; if it throws,
1694
- * the entry is skipped (with the thrown message as the reason) rather than
1695
- * aborting the whole extraction. Omitted by non-agent / human callers.
1954
+ * Per-extracted-path write guard. Called with each entry's
1955
+ * workspace-relative target before it is written; if it throws, the entry
1956
+ * is skipped (with the thrown message as the reason) rather than aborting
1957
+ * the whole extraction. Omitted by non-agent / human callers.
1696
1958
  */
1697
1959
  guardWrite?: (wsRelativePath: string) => Promise<void>,
1698
1960
  ): Promise<UnzipResult> {
@@ -1765,43 +2027,22 @@ export class WorkspaceService implements IWorkspaceService {
1765
2027
  continue;
1766
2028
  }
1767
2029
  processedCount++;
1768
- const rawName = entry.entryName.replace(/\\/g, '/');
1769
-
1770
- if (
1771
- rawName.startsWith('__MACOSX/') ||
1772
- rawName === '__MACOSX' ||
1773
- rawName.endsWith('/.DS_Store') ||
1774
- rawName === '.DS_Store' ||
1775
- /(^|\/)\._/.test(rawName)
1776
- ) {
1777
- continue;
1778
- }
2030
+ const rawName = zipEntryName(entry.entryName);
1779
2031
 
1780
- if (!rawName || rawName.startsWith('/') || /(^|\/)\.\.($|\/)/.test(rawName)) {
1781
- skipped.push({ path: rawName || '(empty)', reason: 'Invalid path' });
1782
- continue;
1783
- }
2032
+ if (isZipNoiseEntry(rawName)) continue;
1784
2033
 
1785
- // An archive never writes into the git folder, whatever the entry's spelling.
1786
- if (hasGitInternalsSegment(rawName)) {
1787
- skipped.push({ path: rawName, reason: new GitInternalsError().message });
2034
+ // What an entry's NAME may be — invalid path, the git folder in any
2035
+ // spelling, a segment no filesystem keeps intact — is `zip-entry-rules`,
2036
+ // so `apply_file_upload` judges the same names by the same rules rather
2037
+ // than by a second copy of them.
2038
+ const nameRefusal = zipEntryNameRefusal(rawName);
2039
+ if (nameRefusal !== null) {
2040
+ skipped.push({ path: rawName || '(empty)', reason: nameRefusal });
1788
2041
  continue;
1789
2042
  }
1790
2043
 
1791
2044
  const trimmed = rawName.replace(/\/+$/, '');
1792
- const segments = trimmed.split('/').filter((s) => s.length > 0);
1793
- let invalidReason: string | null = null;
1794
- for (const segment of segments) {
1795
- const reason = validateFilename(segment);
1796
- if (reason) {
1797
- invalidReason = reason;
1798
- break;
1799
- }
1800
- }
1801
- if (invalidReason) {
1802
- skipped.push({ path: rawName, reason: invalidReason });
1803
- continue;
1804
- }
2045
+ const segments = zipEntrySegments(rawName);
1805
2046
 
1806
2047
  const targetAbsolute = path.resolve(destAbsolute, ...segments);
1807
2048
  const destRoot = path.resolve(destAbsolute);
@@ -1813,17 +2054,17 @@ export class WorkspaceService implements IWorkspaceService {
1813
2054
  const relForReport = path
1814
2055
  .relative(workspaceDir, targetAbsolute)
1815
2056
  .replace(/\\/g, '/');
1816
- // Ontology-session boundary: a write-blocked (or cross-ontology) entry is
1817
- // skipped, not extracted, so an archive can't be a write path around it.
1818
- // Runs before any filesystem side effect — including directory creation —
1819
- // so a blocked entry leaves nothing behind on disk.
2057
+ // An entry the write guard refuses is skipped, not extracted, so an
2058
+ // archive can't be a way around whatever the guard enforces. Runs before
2059
+ // any filesystem side effect — including directory creation — so a
2060
+ // refused entry leaves nothing behind on disk.
1820
2061
  const allowEntry = async (): Promise<boolean> => {
1821
2062
  if (!guardWrite) return true;
1822
2063
  try {
1823
2064
  await guardWrite(relForReport);
1824
2065
  return true;
1825
2066
  } catch (err) {
1826
- skipped.push({ path: rawName, reason: err instanceof Error ? err.message : 'Blocked by the ontology-session boundary' });
2067
+ skipped.push({ path: rawName, reason: err instanceof Error ? err.message : 'Refused before it was written' });
1827
2068
  return false;
1828
2069
  }
1829
2070
  };
@@ -1882,14 +2123,31 @@ export class WorkspaceService implements IWorkspaceService {
1882
2123
  continue;
1883
2124
  }
1884
2125
 
1885
- const data = entry.getData();
1886
- if (data.byteLength > UNZIP_MAX_ENTRY_BYTES) {
2126
+ // Read through the one bounded reader (`readZipEntry`), never with the
2127
+ // declared size above as the only bound: an entry that declares ZERO is
2128
+ // inflated with no cap at all by the archive reader, so a few kilobytes
2129
+ // could expand to whatever memory there is before the checks below ran.
2130
+ // Capped at what is left of both limits, and a read that fails is this
2131
+ // entry's refusal rather than the whole archive's.
2132
+ const read = readZipEntry(
2133
+ entry,
2134
+ Math.min(UNZIP_MAX_ENTRY_BYTES, UNZIP_MAX_TOTAL_BYTES - totalUncompressed),
2135
+ );
2136
+ if (!read.ok && read.reason === 'unreadable') {
2137
+ skipped.push({ path: rawName, reason: `Entry could not be read: ${read.detail}` });
2138
+ continue;
2139
+ }
2140
+ if (!read.ok) {
1887
2141
  skipped.push({
1888
2142
  path: rawName,
1889
- reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
2143
+ reason:
2144
+ totalUncompressed + UNZIP_MAX_ENTRY_BYTES > UNZIP_MAX_TOTAL_BYTES
2145
+ ? `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`
2146
+ : `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
1890
2147
  });
1891
2148
  continue;
1892
2149
  }
2150
+ const data = read.data;
1893
2151
  if (totalUncompressed + data.byteLength > UNZIP_MAX_TOTAL_BYTES) {
1894
2152
  skipped.push({
1895
2153
  path: rawName,