@bevel-software/platform-core-backend 0.15.1 → 0.20.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 (1385) hide show
  1. package/THIRD-PARTY-NOTICES.md +4 -4
  2. package/dist/core/catalog-cache-invalidation.d.ts +16 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
  4. package/dist/core/catalog-cache-invalidation.js +6 -1
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -1
  6. package/dist/core/catalog-revision.d.ts +39 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -0
  8. package/dist/core/catalog-revision.js +94 -0
  9. package/dist/core/catalog-revision.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +7 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +221 -75
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +52 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +222 -87
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +138 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -0
  20. package/dist/core/lifecycle.js +218 -0
  21. package/dist/core/lifecycle.js.map +1 -0
  22. package/dist/core/public-config.d.ts +82 -0
  23. package/dist/core/public-config.d.ts.map +1 -0
  24. package/dist/core/public-config.js +78 -0
  25. package/dist/core/public-config.js.map +1 -0
  26. package/dist/core/readiness.d.ts +86 -0
  27. package/dist/core/readiness.d.ts.map +1 -0
  28. package/dist/core/readiness.js +56 -0
  29. package/dist/core/readiness.js.map +1 -0
  30. package/dist/core-config.d.ts +30 -0
  31. package/dist/core-config.d.ts.map +1 -1
  32. package/dist/core-config.js +64 -2
  33. package/dist/core-config.js.map +1 -1
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/access/access-control.interface.d.ts +215 -11
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +57 -11
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +616 -155
  43. package/dist/modules/access/access-control.service.js.map +1 -1
  44. package/dist/modules/access/access-mutation.service.d.ts +71 -5
  45. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-mutation.service.js +161 -13
  47. package/dist/modules/access/access-mutation.service.js.map +1 -1
  48. package/dist/modules/access/access-view.d.ts +147 -0
  49. package/dist/modules/access/access-view.d.ts.map +1 -0
  50. package/dist/modules/access/access-view.js +211 -0
  51. package/dist/modules/access/access-view.js.map +1 -0
  52. package/dist/modules/access/access.routes.d.ts.map +1 -1
  53. package/dist/modules/access/access.routes.js +255 -79
  54. package/dist/modules/access/access.routes.js.map +1 -1
  55. package/dist/modules/access/admin-locked-commit.d.ts +2 -3
  56. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  57. package/dist/modules/access/admin-locked-commit.js +9 -8
  58. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  59. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
  60. package/dist/modules/access/admin-route-helpers.js +4 -4
  61. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  62. package/dist/modules/access/change-read-gate.d.ts +53 -0
  63. package/dist/modules/access/change-read-gate.d.ts.map +1 -0
  64. package/dist/modules/access/change-read-gate.js +134 -0
  65. package/dist/modules/access/change-read-gate.js.map +1 -0
  66. package/dist/modules/access/creator-access.d.ts +33 -39
  67. package/dist/modules/access/creator-access.d.ts.map +1 -1
  68. package/dist/modules/access/creator-access.js +92 -119
  69. package/dist/modules/access/creator-access.js.map +1 -1
  70. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  71. package/dist/modules/access/directory-sync-bot.js +3 -1
  72. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  73. package/dist/modules/access/groups-admin.service.d.ts +3 -3
  74. package/dist/modules/access/groups-admin.service.js +2 -2
  75. package/dist/modules/access/reference-scan.d.ts +1 -1
  76. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  77. package/dist/modules/access/reference-scan.js +5 -4
  78. package/dist/modules/access/reference-scan.js.map +1 -1
  79. package/dist/modules/access/roles-admin.service.d.ts +72 -13
  80. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  81. package/dist/modules/access/roles-admin.service.js +71 -13
  82. package/dist/modules/access/roles-admin.service.js.map +1 -1
  83. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  84. package/dist/modules/access/synced-groups-committer.js +8 -6
  85. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
  87. package/dist/modules/access/synced-groups-writer.js +3 -8
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  89. package/dist/modules/access/user-access-removal.service.d.ts +209 -0
  90. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
  91. package/dist/modules/access/user-access-removal.service.js +662 -0
  92. package/dist/modules/access/user-access-removal.service.js.map +1 -0
  93. package/dist/modules/access-model/access-errors.d.ts +32 -1
  94. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  95. package/dist/modules/access-model/access-errors.js +38 -11
  96. package/dist/modules/access-model/access-errors.js.map +1 -1
  97. package/dist/modules/access-model/access-grammar.d.ts +49 -24
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +68 -48
  100. package/dist/modules/access-model/access-grammar.js.map +1 -1
  101. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  102. package/dist/modules/access-model/access-splice.js +18 -18
  103. package/dist/modules/access-model/access-splice.js.map +1 -1
  104. package/dist/modules/access-model/access-template.d.ts +22 -0
  105. package/dist/modules/access-model/access-template.d.ts.map +1 -0
  106. package/dist/modules/access-model/access-template.js +42 -0
  107. package/dist/modules/access-model/access-template.js.map +1 -0
  108. package/dist/modules/access-model/change-gate.d.ts +65 -0
  109. package/dist/modules/access-model/change-gate.d.ts.map +1 -0
  110. package/dist/modules/access-model/change-gate.js +25 -0
  111. package/dist/modules/access-model/change-gate.js.map +1 -0
  112. package/dist/modules/access-model/creator.d.ts +13 -16
  113. package/dist/modules/access-model/creator.d.ts.map +1 -1
  114. package/dist/modules/access-model/creator.js.map +1 -1
  115. package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
  116. package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
  117. package/dist/modules/access-model/frontmatter-lines.js +74 -0
  118. package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
  119. package/dist/modules/access-model/plugin-principals.d.ts +5 -3
  120. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -1
  121. package/dist/modules/access-model/plugin-principals.js +11 -17
  122. package/dist/modules/access-model/plugin-principals.js.map +1 -1
  123. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  124. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  125. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  126. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  127. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  128. package/dist/modules/admin/admin-access.service.js +5 -2
  129. package/dist/modules/admin/admin-access.service.js.map +1 -1
  130. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  131. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  132. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  133. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  134. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  135. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  136. package/dist/modules/agent-instructions/compose.js +234 -0
  137. package/dist/modules/agent-instructions/compose.js.map +1 -0
  138. package/dist/modules/agent-instructions/index.d.ts +4 -0
  139. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  140. package/dist/modules/agent-instructions/index.js +4 -0
  141. package/dist/modules/agent-instructions/index.js.map +1 -0
  142. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  143. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  144. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  145. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  146. package/dist/modules/audit/agent-audit.service.d.ts +54 -0
  147. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -0
  148. package/dist/modules/audit/agent-audit.service.js +290 -0
  149. package/dist/modules/audit/agent-audit.service.js.map +1 -0
  150. package/dist/modules/audit/agent-rest-audit.middleware.d.ts +40 -0
  151. package/dist/modules/audit/agent-rest-audit.middleware.d.ts.map +1 -0
  152. package/dist/modules/audit/agent-rest-audit.middleware.js +75 -0
  153. package/dist/modules/audit/agent-rest-audit.middleware.js.map +1 -0
  154. package/dist/modules/audit/audit.contract.d.ts +145 -0
  155. package/dist/modules/audit/audit.contract.d.ts.map +1 -0
  156. package/dist/modules/audit/audit.contract.js +35 -0
  157. package/dist/modules/audit/audit.contract.js.map +1 -0
  158. package/dist/modules/audit/audit.routes.d.ts +21 -0
  159. package/dist/modules/audit/audit.routes.d.ts.map +1 -0
  160. package/dist/modules/audit/audit.routes.js +157 -0
  161. package/dist/modules/audit/audit.routes.js.map +1 -0
  162. package/dist/modules/audit/event-classifier.d.ts +52 -0
  163. package/dist/modules/audit/event-classifier.d.ts.map +1 -0
  164. package/dist/modules/audit/event-classifier.js +123 -0
  165. package/dist/modules/audit/event-classifier.js.map +1 -0
  166. package/dist/modules/audit/index.d.ts +7 -0
  167. package/dist/modules/audit/index.d.ts.map +1 -0
  168. package/dist/modules/audit/index.js +7 -0
  169. package/dist/modules/audit/index.js.map +1 -0
  170. package/dist/modules/audit/request-audit.d.ts +57 -0
  171. package/dist/modules/audit/request-audit.d.ts.map +1 -0
  172. package/dist/modules/audit/request-audit.js +117 -0
  173. package/dist/modules/audit/request-audit.js.map +1 -0
  174. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  175. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  176. package/dist/modules/auth/account-erasure.service.js +96 -10
  177. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  178. package/dist/modules/auth/account.routes.d.ts +6 -3
  179. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  180. package/dist/modules/auth/account.routes.js +109 -10
  181. package/dist/modules/auth/account.routes.js.map +1 -1
  182. package/dist/modules/auth/auth.routes.d.ts +10 -3
  183. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  184. package/dist/modules/auth/auth.routes.js +9 -4
  185. package/dist/modules/auth/auth.routes.js.map +1 -1
  186. package/dist/modules/auth/auth.service.d.ts +89 -5
  187. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  188. package/dist/modules/auth/auth.service.js +160 -25
  189. package/dist/modules/auth/auth.service.js.map +1 -1
  190. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  191. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  192. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  193. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  194. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  195. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  196. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  197. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  198. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  199. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  200. package/dist/modules/database/advisory-lock.d.ts +103 -0
  201. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  202. package/dist/modules/database/advisory-lock.js +320 -0
  203. package/dist/modules/database/advisory-lock.js.map +1 -0
  204. package/dist/modules/database/connection.d.ts.map +1 -1
  205. package/dist/modules/database/connection.js +9 -1
  206. package/dist/modules/database/connection.js.map +1 -1
  207. package/dist/modules/database/core-schema.d.ts +859 -99
  208. package/dist/modules/database/core-schema.d.ts.map +1 -1
  209. package/dist/modules/database/core-schema.js +218 -0
  210. package/dist/modules/database/core-schema.js.map +1 -1
  211. package/dist/modules/database/migrate.d.ts.map +1 -1
  212. package/dist/modules/database/migrate.js +26 -6
  213. package/dist/modules/database/migrate.js.map +1 -1
  214. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  215. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  216. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  217. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  218. package/dist/modules/diff/diff.routes.js +2 -4
  219. package/dist/modules/diff/diff.routes.js.map +1 -1
  220. package/dist/modules/diff/diff.service.d.ts +4 -1
  221. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  222. package/dist/modules/diff/diff.service.js +84 -81
  223. package/dist/modules/diff/diff.service.js.map +1 -1
  224. package/dist/modules/{workspace → kb-fs}/bevel-ignore.d.ts +14 -9
  225. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  226. package/dist/modules/{workspace → kb-fs}/bevel-ignore.js +24 -14
  227. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  228. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  229. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  230. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  231. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +45 -0
  232. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  233. package/dist/modules/kb-fs/git-guarded-filesystem.js +117 -0
  234. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  235. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  236. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  237. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  238. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  239. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  240. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  241. package/dist/modules/kb-fs/node-fs.js +158 -0
  242. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  243. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  244. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  245. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  246. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  247. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  248. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  249. package/dist/modules/kb-fs/repo-path.js +195 -3
  250. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  251. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  252. package/dist/modules/kb-sync/kb-sync.routes.js +4 -2
  253. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  254. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  255. package/dist/modules/kb-sync/kb-sync.service.js +7 -5
  256. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  257. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -1
  258. package/dist/modules/marketplace/git-http.routes.js +6 -4
  259. package/dist/modules/marketplace/git-http.routes.js.map +1 -1
  260. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +12 -2
  261. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -1
  262. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +32 -3
  263. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -1
  264. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +27 -0
  265. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -1
  266. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +45 -0
  267. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -1
  268. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -1
  269. package/dist/modules/marketplace/github-facade/github-facade.routes.js +30 -6
  270. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -1
  271. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +21 -9
  272. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -1
  273. package/dist/modules/marketplace/github-facade/github-facade.service.js +60 -19
  274. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -1
  275. package/dist/modules/marketplace/github-facade/index.d.ts +1 -1
  276. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -1
  277. package/dist/modules/marketplace/github-facade/index.js +1 -1
  278. package/dist/modules/marketplace/github-facade/index.js.map +1 -1
  279. package/dist/modules/marketplace/marketplace-repo.service.d.ts +14 -1
  280. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -1
  281. package/dist/modules/marketplace/marketplace-repo.service.js +26 -8
  282. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -1
  283. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  284. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  285. package/dist/modules/mcp/downstream-pool.js +221 -0
  286. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  287. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  288. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  289. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  290. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  291. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  292. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  293. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  294. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  295. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  296. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  297. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  298. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  299. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  300. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  301. package/dist/modules/mcp/first-call-probe.js +298 -0
  302. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  303. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  304. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  305. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  306. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  307. package/dist/modules/mcp/mcp-auth.middleware.d.ts +10 -5
  308. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  309. package/dist/modules/mcp/mcp-auth.middleware.js +56 -16
  310. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  311. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  312. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  313. package/dist/modules/mcp/mcp.routes.js +91 -180
  314. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  315. package/dist/modules/mcp/mcp.service.d.ts +226 -61
  316. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  317. package/dist/modules/mcp/mcp.service.js +758 -161
  318. package/dist/modules/mcp/mcp.service.js.map +1 -1
  319. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +59 -0
  320. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  321. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +184 -10
  322. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  323. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  324. package/dist/modules/mcp/oauth/oauth-consent.routes.js +3 -1
  325. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  326. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  327. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  328. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  329. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  330. package/dist/modules/plugins/compile/compile-marketplace.d.ts +13 -6
  331. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -1
  332. package/dist/modules/plugins/compile/compile-marketplace.js +119 -36
  333. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -1
  334. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +8 -1
  335. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -1
  336. package/dist/modules/plugins/compile/marketplace-compiler.service.js +9 -6
  337. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -1
  338. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -1
  339. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +32 -9
  340. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -1
  341. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +26 -6
  342. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -1
  343. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +71 -14
  344. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -1
  345. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +4 -2
  346. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -1
  347. package/dist/modules/plugins/discovery/kb-plugin-source.js +9 -6
  348. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -1
  349. package/dist/modules/plugins/discovery/native.source.d.ts +2 -1
  350. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -1
  351. package/dist/modules/plugins/discovery/native.source.js +22 -4
  352. package/dist/modules/plugins/discovery/native.source.js.map +1 -1
  353. package/dist/modules/plugins/discovery/plugin-source.d.ts +17 -3
  354. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -1
  355. package/dist/modules/plugins/index.d.ts +6 -3
  356. package/dist/modules/plugins/index.d.ts.map +1 -1
  357. package/dist/modules/plugins/index.js +6 -3
  358. package/dist/modules/plugins/index.js.map +1 -1
  359. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  360. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  361. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  362. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  363. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  364. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  365. package/dist/modules/plugins/join-request-records.store.js +179 -0
  366. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  367. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  368. package/dist/modules/plugins/join-requests.service.js +4 -2
  369. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  370. package/dist/modules/plugins/plugin-links.d.ts +4 -1
  371. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  372. package/dist/modules/plugins/plugin-links.js +6 -3
  373. package/dist/modules/plugins/plugin-links.js.map +1 -1
  374. package/dist/modules/plugins/plugin-links.service.d.ts +2 -5
  375. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -1
  376. package/dist/modules/plugins/plugin-links.service.js +4 -13
  377. package/dist/modules/plugins/plugin-links.service.js.map +1 -1
  378. package/dist/modules/plugins/plugin-provision.service.d.ts +91 -19
  379. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  380. package/dist/modules/plugins/plugin-provision.service.js +183 -38
  381. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  382. package/dist/modules/plugins/plugin-rename.service.d.ts +3 -1
  383. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -1
  384. package/dist/modules/plugins/plugin-rename.service.js +20 -11
  385. package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
  386. package/dist/modules/plugins/plugins.contract.d.ts +52 -4
  387. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  388. package/dist/modules/plugins/plugins.routes.d.ts +38 -5
  389. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  390. package/dist/modules/plugins/plugins.routes.js +190 -116
  391. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  392. package/dist/modules/plugins/plugins.service.d.ts +38 -6
  393. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  394. package/dist/modules/plugins/plugins.service.js +86 -17
  395. package/dist/modules/plugins/plugins.service.js.map +1 -1
  396. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  397. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  398. package/dist/modules/plugins/plugins.tools.js +82 -0
  399. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  400. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  401. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  402. package/dist/modules/plugins/teams.routes.js +79 -0
  403. package/dist/modules/plugins/teams.routes.js.map +1 -0
  404. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  405. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  406. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  407. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  408. package/dist/modules/secrets-vault/index.d.ts +1 -1
  409. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  410. package/dist/modules/secrets-vault/index.js.map +1 -1
  411. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  412. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  413. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  414. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  415. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  416. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  417. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  418. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  419. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  420. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  421. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  422. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  423. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  424. package/dist/modules/settings/connection-check.d.ts +118 -0
  425. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  426. package/dist/modules/settings/connection-check.js +287 -0
  427. package/dist/modules/settings/connection-check.js.map +1 -0
  428. package/dist/modules/settings/deployment-settings.service.d.ts +162 -8
  429. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  430. package/dist/modules/settings/deployment-settings.service.js +375 -52
  431. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  432. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  433. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  434. package/dist/modules/settings/git-root-folders.js +130 -0
  435. package/dist/modules/settings/git-root-folders.js.map +1 -0
  436. package/dist/modules/settings/oidc-check.d.ts +85 -0
  437. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  438. package/dist/modules/settings/oidc-check.js +172 -0
  439. package/dist/modules/settings/oidc-check.js.map +1 -0
  440. package/dist/modules/settings/setup.routes.d.ts +31 -1
  441. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  442. package/dist/modules/settings/setup.routes.js +400 -108
  443. package/dist/modules/settings/setup.routes.js.map +1 -1
  444. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  445. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  446. package/dist/modules/skills/allowed-tools-check.js +260 -0
  447. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  448. package/dist/modules/skills/index.d.ts +3 -1
  449. package/dist/modules/skills/index.d.ts.map +1 -1
  450. package/dist/modules/skills/index.js +1 -0
  451. package/dist/modules/skills/index.js.map +1 -1
  452. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  453. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  454. package/dist/modules/skills/pending-skills.service.js +18 -70
  455. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  456. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  457. package/dist/modules/skills/skill-access-requests.routes.js +9 -6
  458. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  459. package/dist/modules/skills/skills.contract.d.ts +44 -10
  460. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  461. package/dist/modules/skills/skills.routes.d.ts +8 -1
  462. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  463. package/dist/modules/skills/skills.routes.js +16 -2
  464. package/dist/modules/skills/skills.routes.js.map +1 -1
  465. package/dist/modules/skills/skills.service.d.ts +31 -7
  466. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  467. package/dist/modules/skills/skills.service.js +191 -81
  468. package/dist/modules/skills/skills.service.js.map +1 -1
  469. package/dist/modules/skills/skills.tools.d.ts +4 -1
  470. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  471. package/dist/modules/skills/skills.tools.js +41 -6
  472. package/dist/modules/skills/skills.tools.js.map +1 -1
  473. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  474. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  475. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  476. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  477. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  478. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  479. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  480. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  481. package/dist/modules/tool-auth/external-api-key.interface.d.ts +72 -8
  482. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  483. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  484. package/dist/modules/tool-auth/external-api-key.service.d.ts +17 -6
  485. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  486. package/dist/modules/tool-auth/external-api-key.service.js +83 -27
  487. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  488. package/dist/modules/tool-auth/internal-token.service.d.ts +15 -2
  489. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  490. package/dist/modules/tool-auth/internal-token.service.js +6 -2
  491. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  492. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  493. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  494. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  495. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  496. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +15 -1
  497. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  498. package/dist/modules/tool-auth/tool-auth.middleware.js +17 -6
  499. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  500. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  501. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  502. package/dist/modules/tool-helpers/tool-context.js +28 -6
  503. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  504. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  505. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  506. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  507. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  508. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  509. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  510. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  511. package/dist/modules/tool-manuals/index.d.ts +2 -1
  512. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  513. package/dist/modules/tool-manuals/index.js +1 -0
  514. package/dist/modules/tool-manuals/index.js.map +1 -1
  515. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  516. package/dist/modules/tool-manuals/mcp-json-discovery.js +8 -6
  517. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  518. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  519. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  520. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  521. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  522. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  523. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  524. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  525. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  526. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  527. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  528. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  529. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  530. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  531. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  532. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  533. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  534. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  535. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  536. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  537. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  538. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +119 -5
  539. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  540. package/dist/modules/tool-manuals/tool-manuals.service.js +404 -74
  541. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  542. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  543. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  544. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  545. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  546. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  547. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  548. package/dist/modules/workflow/event-bus.d.ts +10 -4
  549. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  550. package/dist/modules/workflow/event-bus.js +14 -11
  551. package/dist/modules/workflow/event-bus.js.map +1 -1
  552. package/dist/modules/workflow/events.routes.d.ts +17 -2
  553. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  554. package/dist/modules/workflow/events.routes.js +31 -12
  555. package/dist/modules/workflow/events.routes.js.map +1 -1
  556. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  557. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  558. package/dist/modules/workflow/file-lock.service.js +27 -4
  559. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  560. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  561. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  562. package/dist/modules/workflow/git/change-request-link.js +34 -0
  563. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  564. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  565. package/dist/modules/workflow/git/git-version.js +3 -1
  566. package/dist/modules/workflow/git/git-version.js.map +1 -1
  567. package/dist/modules/workflow/git/git.service.d.ts +263 -4
  568. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  569. package/dist/modules/workflow/git/git.service.js +528 -118
  570. package/dist/modules/workflow/git/git.service.js.map +1 -1
  571. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  572. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  573. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  574. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  575. package/dist/modules/workflow/git/pull-request.service.d.ts +51 -2
  576. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  577. package/dist/modules/workflow/git/pull-request.service.js +157 -25
  578. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  579. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  580. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  581. package/dist/modules/workflow/pending-commits.service.js +24 -4
  582. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  583. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  584. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  585. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  586. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  587. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  588. package/dist/modules/workflow/recovery-bot.js +3 -1
  589. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  590. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  591. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  592. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  593. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  594. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  595. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  596. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  597. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  598. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  599. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  600. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  601. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  602. package/dist/modules/workflow/sanitize-error.js +8 -4
  603. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  604. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  605. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  606. package/dist/modules/workflow/workflow.routes.js +259 -29
  607. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  608. package/dist/modules/workflow/workflow.service.d.ts +166 -14
  609. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  610. package/dist/modules/workflow/workflow.service.js +906 -106
  611. package/dist/modules/workflow/workflow.service.js.map +1 -1
  612. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  613. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  614. package/dist/modules/workspace/empty-dirs.js +90 -0
  615. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  616. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  617. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  618. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  619. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  620. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  621. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  622. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  623. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  624. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  625. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  626. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  627. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  628. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  629. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  630. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  631. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  632. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  633. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  634. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  635. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  636. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  637. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  638. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  639. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  640. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  641. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  642. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  643. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  644. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  645. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  646. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  647. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  648. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  649. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  650. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  651. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  652. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  653. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  654. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  655. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  656. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  657. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  658. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  659. package/dist/modules/workspace/not-found.d.ts +80 -0
  660. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  661. package/dist/modules/workspace/not-found.js +112 -0
  662. package/dist/modules/workspace/not-found.js.map +1 -0
  663. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  664. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  665. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  666. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  667. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  668. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  669. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  670. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  671. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  672. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  673. package/dist/modules/workspace/startup/kb-git.js +34 -35
  674. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  675. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  676. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  677. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  678. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  679. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  680. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  681. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  682. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  683. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  684. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  685. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  686. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  687. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  688. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  689. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  690. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  691. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  692. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  693. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  694. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  695. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +7 -1
  696. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -1
  697. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +45 -81
  698. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -1
  699. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  700. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  701. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  702. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  703. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  704. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  705. package/dist/modules/workspace/startup/steps/seed-tree.js +154 -78
  706. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  707. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  708. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  709. package/dist/modules/workspace/startup/steps/template-files.step.js +446 -138
  710. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  711. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  712. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  713. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  714. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  715. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  716. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  717. package/dist/modules/workspace/workspace.routes.js +649 -238
  718. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  719. package/dist/modules/workspace/workspace.service.d.ts +298 -12
  720. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  721. package/dist/modules/workspace/workspace.service.js +933 -269
  722. package/dist/modules/workspace/workspace.service.js.map +1 -1
  723. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  724. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  725. package/dist/modules/workspace/workspace.tools.js +1626 -146
  726. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  727. package/dist/modules/workspace/write-denial.d.ts +76 -0
  728. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  729. package/dist/modules/workspace/write-denial.js +138 -0
  730. package/dist/modules/workspace/write-denial.js.map +1 -0
  731. package/dist/shared/canonical-file-identity.d.ts +64 -0
  732. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  733. package/dist/shared/canonical-file-identity.js +92 -0
  734. package/dist/shared/canonical-file-identity.js.map +1 -0
  735. package/dist/shared/domain-errors.d.ts +108 -0
  736. package/dist/shared/domain-errors.d.ts.map +1 -1
  737. package/dist/shared/domain-errors.js +137 -0
  738. package/dist/shared/domain-errors.js.map +1 -1
  739. package/dist/shared/email-identity.d.ts +32 -0
  740. package/dist/shared/email-identity.d.ts.map +1 -0
  741. package/dist/shared/email-identity.js +37 -0
  742. package/dist/shared/email-identity.js.map +1 -0
  743. package/dist/shared/fs.contract.d.ts +206 -0
  744. package/dist/shared/fs.contract.d.ts.map +1 -0
  745. package/dist/shared/fs.contract.js +79 -0
  746. package/dist/shared/fs.contract.js.map +1 -0
  747. package/dist/shared/git-failure.d.ts +80 -0
  748. package/dist/shared/git-failure.d.ts.map +1 -0
  749. package/dist/shared/git-failure.js +139 -0
  750. package/dist/shared/git-failure.js.map +1 -0
  751. package/dist/shared/git-internals.d.ts +23 -0
  752. package/dist/shared/git-internals.d.ts.map +1 -0
  753. package/dist/shared/git-internals.js +181 -0
  754. package/dist/shared/git-internals.js.map +1 -0
  755. package/dist/shared/git.contract.d.ts +157 -0
  756. package/dist/shared/git.contract.d.ts.map +1 -0
  757. package/dist/shared/git.contract.js +103 -0
  758. package/dist/shared/git.contract.js.map +1 -0
  759. package/dist/shared/http-errors.d.ts +33 -0
  760. package/dist/shared/http-errors.d.ts.map +1 -0
  761. package/dist/shared/http-errors.js +35 -0
  762. package/dist/shared/http-errors.js.map +1 -0
  763. package/dist/shared/logger.contract.d.ts +63 -0
  764. package/dist/shared/logger.contract.d.ts.map +1 -0
  765. package/dist/shared/logger.contract.js +126 -0
  766. package/dist/shared/logger.contract.js.map +1 -0
  767. package/dist/shared/logging.d.ts +31 -0
  768. package/dist/shared/logging.d.ts.map +1 -0
  769. package/dist/shared/logging.js +92 -0
  770. package/dist/shared/logging.js.map +1 -0
  771. package/dist/shared/path-containment.d.ts +21 -0
  772. package/dist/shared/path-containment.d.ts.map +1 -0
  773. package/dist/shared/path-containment.js +34 -0
  774. package/dist/shared/path-containment.js.map +1 -0
  775. package/dist/shared/pending-proposals.d.ts +71 -0
  776. package/dist/shared/pending-proposals.d.ts.map +1 -0
  777. package/dist/shared/pending-proposals.js +98 -0
  778. package/dist/shared/pending-proposals.js.map +1 -0
  779. package/dist/shared/printable.d.ts +22 -0
  780. package/dist/shared/printable.d.ts.map +1 -0
  781. package/dist/shared/printable.js +65 -0
  782. package/dist/shared/printable.js.map +1 -0
  783. package/dist/shared/redact-secret.d.ts +30 -0
  784. package/dist/shared/redact-secret.d.ts.map +1 -0
  785. package/dist/shared/redact-secret.js +114 -0
  786. package/dist/shared/redact-secret.js.map +1 -0
  787. package/dist/shared/rename-no-replace.d.ts +119 -0
  788. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  789. package/dist/shared/rename-no-replace.js +323 -0
  790. package/dist/shared/rename-no-replace.js.map +1 -0
  791. package/dist/shared/ssrf.d.ts.map +1 -1
  792. package/dist/shared/ssrf.js +49 -18
  793. package/dist/shared/ssrf.js.map +1 -1
  794. package/dist/version.d.ts +10 -6
  795. package/dist/version.d.ts.map +1 -1
  796. package/dist/version.js +10 -6
  797. package/dist/version.js.map +1 -1
  798. package/kb-template/.bevelignore +4 -3
  799. package/kb-template/AGENTS.md +281 -36
  800. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  801. package/kb-template/access.md +5 -3
  802. package/kb-template/mcp-description.md +27 -0
  803. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  804. package/migrations/0007_github_facade_registered.sql +1 -0
  805. package/migrations/0008_change_request_apply_failure.sql +4 -0
  806. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  807. package/migrations/0010_plugin_join_requests.sql +15 -0
  808. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  809. package/migrations/0012_agent_audit.sql +41 -0
  810. package/migrations/0013_agent_identity_and_key_soft_delete.sql +29 -0
  811. package/migrations/meta/0006_snapshot.json +1723 -0
  812. package/migrations/meta/0007_snapshot.json +1729 -0
  813. package/migrations/meta/0008_snapshot.json +1753 -0
  814. package/migrations/meta/0009_snapshot.json +1759 -0
  815. package/migrations/meta/0010_snapshot.json +1872 -0
  816. package/migrations/meta/0011_snapshot.json +1884 -0
  817. package/migrations/meta/0012_snapshot.json +2247 -0
  818. package/migrations/meta/0013_snapshot.json +2259 -0
  819. package/migrations/meta/_journal.json +56 -0
  820. package/package.json +7 -6
  821. package/src/__tests__/core-config.domain.test.ts +67 -2
  822. package/src/__tests__/kb-layout-config.test.ts +186 -1
  823. package/src/__tests__/open-change-gate.ts +15 -0
  824. package/src/core/__tests__/catalog-cache-invalidation.test.ts +83 -3
  825. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  826. package/src/core/__tests__/lifecycle.test.ts +369 -0
  827. package/src/core/__tests__/public-config.test.ts +30 -0
  828. package/src/core/__tests__/readiness.test.ts +109 -0
  829. package/src/core/catalog-cache-invalidation.ts +20 -1
  830. package/src/core/catalog-revision.ts +110 -0
  831. package/src/core/create-core-server.ts +286 -79
  832. package/src/core/create-core-services.ts +315 -81
  833. package/src/core/lifecycle.ts +312 -0
  834. package/src/core/public-config.ts +86 -0
  835. package/src/core/readiness.ts +123 -0
  836. package/src/core-config.ts +70 -4
  837. package/src/index.ts +15 -0
  838. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  839. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  840. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  841. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  842. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  843. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  844. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  845. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +2 -0
  846. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  847. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  848. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  849. package/src/modules/access/__tests__/access-control.service.test.ts +123 -78
  850. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  851. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  852. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  853. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  854. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  855. package/src/modules/access/__tests__/access-plugin-principals.test.ts +18 -1
  856. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  857. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  858. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  859. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  860. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  861. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  862. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  863. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  864. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  865. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  866. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  867. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  868. package/src/modules/access/__tests__/groups-admin.service.test.ts +6 -5
  869. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  870. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  871. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  872. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  873. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  874. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  875. package/src/modules/access/access-control.interface.ts +251 -11
  876. package/src/modules/access/access-control.service.ts +719 -164
  877. package/src/modules/access/access-mutation.service.ts +182 -13
  878. package/src/modules/access/access-view.ts +316 -0
  879. package/src/modules/access/access.routes.ts +293 -95
  880. package/src/modules/access/admin-locked-commit.ts +12 -14
  881. package/src/modules/access/admin-route-helpers.ts +4 -4
  882. package/src/modules/access/change-read-gate.ts +148 -0
  883. package/src/modules/access/creator-access.ts +89 -124
  884. package/src/modules/access/directory-sync-bot.ts +4 -1
  885. package/src/modules/access/groups-admin.service.ts +3 -3
  886. package/src/modules/access/reference-scan.ts +5 -5
  887. package/src/modules/access/roles-admin.service.ts +94 -15
  888. package/src/modules/access/synced-groups-committer.ts +10 -15
  889. package/src/modules/access/synced-groups-writer.ts +3 -12
  890. package/src/modules/access/user-access-removal.service.ts +711 -0
  891. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  892. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  893. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  894. package/src/modules/access-model/access-errors.ts +56 -15
  895. package/src/modules/access-model/access-grammar.ts +85 -48
  896. package/src/modules/access-model/access-splice.ts +18 -18
  897. package/src/modules/access-model/access-template.ts +44 -0
  898. package/src/modules/access-model/change-gate.ts +80 -0
  899. package/src/modules/access-model/creator.ts +18 -21
  900. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  901. package/src/modules/access-model/plugin-principals.ts +12 -19
  902. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  903. package/src/modules/admin/admin-access.service.ts +6 -2
  904. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  905. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  906. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  907. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  908. package/src/modules/agent-instructions/compose.ts +271 -0
  909. package/src/modules/agent-instructions/index.ts +14 -0
  910. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  911. package/src/modules/audit/__tests__/agent-audit.service.test.ts +370 -0
  912. package/src/modules/audit/__tests__/agent-rest-audit.middleware.test.ts +113 -0
  913. package/src/modules/audit/__tests__/audit.routes.test.ts +201 -0
  914. package/src/modules/audit/__tests__/event-classifier.test.ts +121 -0
  915. package/src/modules/audit/agent-audit.service.ts +316 -0
  916. package/src/modules/audit/agent-rest-audit.middleware.ts +83 -0
  917. package/src/modules/audit/audit.contract.ts +164 -0
  918. package/src/modules/audit/audit.routes.ts +168 -0
  919. package/src/modules/audit/event-classifier.ts +155 -0
  920. package/src/modules/audit/index.ts +14 -0
  921. package/src/modules/audit/request-audit.ts +141 -0
  922. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  923. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  924. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  925. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  926. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  927. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  928. package/src/modules/auth/account-erasure.service.ts +117 -10
  929. package/src/modules/auth/account.routes.ts +115 -14
  930. package/src/modules/auth/auth.routes.ts +20 -7
  931. package/src/modules/auth/auth.service.ts +187 -28
  932. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  933. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  934. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  935. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  936. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  937. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  938. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  939. package/src/modules/database/advisory-lock.ts +346 -0
  940. package/src/modules/database/connection.ts +9 -1
  941. package/src/modules/database/core-schema.ts +227 -0
  942. package/src/modules/database/migrate.ts +27 -6
  943. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  944. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  945. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  946. package/src/modules/diff/diff.routes.ts +2 -4
  947. package/src/modules/diff/diff.service.ts +80 -79
  948. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  949. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  950. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  951. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  952. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  953. package/src/modules/kb-fs/git-guarded-filesystem.ts +136 -0
  954. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  955. package/src/modules/kb-fs/node-fs.ts +165 -0
  956. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  957. package/src/modules/kb-fs/repo-path.ts +213 -3
  958. package/src/modules/kb-sync/kb-sync.routes.ts +5 -2
  959. package/src/modules/kb-sync/kb-sync.service.ts +8 -5
  960. package/src/modules/marketplace/__tests__/github-facade.test.ts +134 -2
  961. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +2 -1
  962. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  963. package/src/modules/marketplace/git-http.routes.ts +7 -4
  964. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +35 -3
  965. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +55 -0
  966. package/src/modules/marketplace/github-facade/github-facade.routes.ts +32 -6
  967. package/src/modules/marketplace/github-facade/github-facade.service.ts +80 -19
  968. package/src/modules/marketplace/github-facade/index.ts +1 -0
  969. package/src/modules/marketplace/marketplace-repo.service.ts +23 -8
  970. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +183 -13
  971. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  972. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  973. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  974. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  975. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  976. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  977. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +68 -7
  978. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  979. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  980. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +24 -4
  981. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  982. package/src/modules/mcp/__tests__/mcp.service.test.ts +391 -6
  983. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  984. package/src/modules/mcp/downstream-pool.ts +256 -0
  985. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  986. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  987. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  988. package/src/modules/mcp/first-call-probe.ts +418 -0
  989. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  990. package/src/modules/mcp/mcp-auth.middleware.ts +56 -16
  991. package/src/modules/mcp/mcp.routes.ts +386 -482
  992. package/src/modules/mcp/mcp.service.ts +905 -177
  993. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +189 -8
  994. package/src/modules/mcp/oauth/oauth-consent.routes.ts +4 -4
  995. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  996. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +10 -7
  997. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  998. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +139 -10
  999. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  1000. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  1001. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  1002. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +148 -7
  1003. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +10 -13
  1004. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  1005. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +129 -19
  1006. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +91 -29
  1007. package/src/modules/plugins/__tests__/plugins.routes.test.ts +370 -19
  1008. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  1009. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  1010. package/src/modules/plugins/compile/compile-marketplace.ts +143 -46
  1011. package/src/modules/plugins/compile/marketplace-compiler.service.ts +8 -7
  1012. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +56 -20
  1013. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +30 -9
  1014. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +93 -18
  1015. package/src/modules/plugins/discovery/kb-plugin-source.ts +10 -7
  1016. package/src/modules/plugins/discovery/native.source.ts +21 -3
  1017. package/src/modules/plugins/discovery/plugin-source.ts +17 -3
  1018. package/src/modules/plugins/index.ts +16 -3
  1019. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  1020. package/src/modules/plugins/join-request-records.store.ts +353 -0
  1021. package/src/modules/plugins/join-requests.service.ts +7 -4
  1022. package/src/modules/plugins/plugin-links.service.ts +4 -17
  1023. package/src/modules/plugins/plugin-links.ts +6 -3
  1024. package/src/modules/plugins/plugin-provision.service.ts +209 -37
  1025. package/src/modules/plugins/plugin-rename.service.ts +21 -9
  1026. package/src/modules/plugins/plugins.contract.ts +52 -4
  1027. package/src/modules/plugins/plugins.routes.ts +204 -124
  1028. package/src/modules/plugins/plugins.service.ts +92 -18
  1029. package/src/modules/plugins/plugins.tools.ts +88 -0
  1030. package/src/modules/plugins/teams.routes.ts +129 -0
  1031. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  1032. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  1033. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  1034. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  1035. package/src/modules/secrets-vault/index.ts +1 -0
  1036. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  1037. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  1038. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  1039. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  1040. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  1041. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +223 -6
  1042. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  1043. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  1044. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  1045. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1046. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1047. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1048. package/src/modules/settings/connection-check.ts +351 -0
  1049. package/src/modules/settings/deployment-settings.service.ts +444 -59
  1050. package/src/modules/settings/git-root-folders.ts +171 -0
  1051. package/src/modules/settings/oidc-check.ts +229 -0
  1052. package/src/modules/settings/setup.routes.ts +456 -110
  1053. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1054. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1055. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1056. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1057. package/src/modules/skills/__tests__/skills.service.test.ts +223 -2
  1058. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1059. package/src/modules/skills/index.ts +4 -0
  1060. package/src/modules/skills/pending-skills.service.ts +21 -82
  1061. package/src/modules/skills/skill-access-requests.routes.ts +10 -6
  1062. package/src/modules/skills/skills.contract.ts +43 -11
  1063. package/src/modules/skills/skills.routes.ts +16 -1
  1064. package/src/modules/skills/skills.service.ts +214 -84
  1065. package/src/modules/skills/skills.tools.ts +43 -5
  1066. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1067. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1068. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +153 -8
  1069. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +6 -0
  1070. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1071. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1072. package/src/modules/tool-auth/external-api-key.interface.ts +78 -8
  1073. package/src/modules/tool-auth/external-api-key.service.ts +98 -33
  1074. package/src/modules/tool-auth/internal-token.service.ts +16 -2
  1075. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1076. package/src/modules/tool-auth/tool-auth.middleware.ts +33 -8
  1077. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1078. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1079. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1080. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1081. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1082. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1083. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1084. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1085. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1086. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1087. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +9 -4
  1088. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1089. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1090. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1091. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1092. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1093. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1094. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1095. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1096. package/src/modules/tool-manuals/index.ts +3 -0
  1097. package/src/modules/tool-manuals/mcp-json-discovery.ts +13 -10
  1098. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1099. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1100. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1101. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1102. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1103. package/src/modules/tool-manuals/tool-manuals.service.ts +432 -87
  1104. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1105. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1106. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1107. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1108. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1109. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1110. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1111. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1112. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +2 -0
  1113. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1114. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1115. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1116. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1117. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1118. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +2 -0
  1119. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1120. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +294 -25
  1121. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1122. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1123. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1124. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +14 -0
  1125. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +5 -0
  1126. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1127. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1128. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1129. package/src/modules/workflow/event-bus.ts +26 -28
  1130. package/src/modules/workflow/events.routes.ts +56 -21
  1131. package/src/modules/workflow/file-lock.service.ts +27 -4
  1132. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1133. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1134. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1135. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1136. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1137. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1138. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1139. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1140. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1141. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1142. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1143. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1144. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1145. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1146. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1147. package/src/modules/workflow/git/git-version.ts +5 -2
  1148. package/src/modules/workflow/git/git.service.ts +684 -144
  1149. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1150. package/src/modules/workflow/git/pull-request.service.ts +179 -27
  1151. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1152. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1153. package/src/modules/workflow/recovery-bot.ts +4 -1
  1154. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1155. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1156. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1157. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1158. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1159. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1160. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1161. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1162. package/src/modules/workflow/sanitize-error.ts +9 -4
  1163. package/src/modules/workflow/workflow.routes.ts +273 -29
  1164. package/src/modules/workflow/workflow.service.ts +1087 -171
  1165. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1166. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1167. package/src/modules/workspace/__tests__/git-internals.security.test.ts +883 -0
  1168. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1169. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1170. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1171. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1172. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1173. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1174. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1175. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1176. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1177. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +25 -6
  1178. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1179. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -5
  1180. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1181. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +56 -1
  1182. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1183. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1184. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1185. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1186. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1187. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1188. package/src/modules/workspace/empty-dirs.ts +79 -0
  1189. package/src/modules/workspace/file-readers/README.md +32 -0
  1190. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1191. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1192. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1193. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1194. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1195. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1196. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1197. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1198. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1199. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1200. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1201. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1202. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1203. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1204. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1205. package/src/modules/workspace/not-found.ts +119 -0
  1206. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1207. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1208. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1209. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1210. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1211. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1212. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1213. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1215 -73
  1214. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1215. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1216. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1217. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1218. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +46 -85
  1219. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1220. package/src/modules/workspace/startup/steps/seed-tree.ts +159 -78
  1221. package/src/modules/workspace/startup/steps/template-files.step.ts +492 -141
  1222. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1223. package/src/modules/workspace/workspace.routes.ts +687 -257
  1224. package/src/modules/workspace/workspace.service.ts +1000 -303
  1225. package/src/modules/workspace/workspace.tools.ts +1823 -180
  1226. package/src/modules/workspace/write-denial.ts +189 -0
  1227. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1228. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1229. package/src/shared/__tests__/filename.test.ts +0 -0
  1230. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1231. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1232. package/src/shared/__tests__/git-internals.test.ts +156 -0
  1233. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1234. package/src/shared/__tests__/logging.test.ts +185 -0
  1235. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1236. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1237. package/src/shared/__tests__/printable.test.ts +64 -0
  1238. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1239. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1240. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1241. package/src/shared/canonical-file-identity.ts +92 -0
  1242. package/src/shared/domain-errors.ts +149 -0
  1243. package/src/shared/email-identity.ts +38 -0
  1244. package/src/shared/fs.contract.ts +225 -0
  1245. package/src/shared/git-failure.ts +196 -0
  1246. package/src/shared/git-internals.ts +179 -0
  1247. package/src/shared/git.contract.ts +170 -0
  1248. package/src/shared/http-errors.ts +36 -0
  1249. package/src/shared/logger.contract.ts +141 -0
  1250. package/src/shared/logging.ts +96 -0
  1251. package/src/shared/path-containment.ts +34 -0
  1252. package/src/shared/pending-proposals.ts +161 -0
  1253. package/src/shared/printable.ts +68 -0
  1254. package/src/shared/redact-secret.ts +118 -0
  1255. package/src/shared/rename-no-replace.ts +386 -0
  1256. package/src/shared/ssrf.ts +44 -16
  1257. package/src/version.ts +10 -6
  1258. package/dist/modules/access/access-errors.d.ts +0 -34
  1259. package/dist/modules/access/access-errors.d.ts.map +0 -1
  1260. package/dist/modules/access/access-errors.js +0 -40
  1261. package/dist/modules/access/access-errors.js.map +0 -1
  1262. package/dist/modules/access/access-splice.d.ts +0 -140
  1263. package/dist/modules/access/access-splice.d.ts.map +0 -1
  1264. package/dist/modules/access/access-splice.js +0 -389
  1265. package/dist/modules/access/access-splice.js.map +0 -1
  1266. package/dist/modules/access/group-files.d.ts +0 -83
  1267. package/dist/modules/access/group-files.d.ts.map +0 -1
  1268. package/dist/modules/access/group-files.js +0 -167
  1269. package/dist/modules/access/group-files.js.map +0 -1
  1270. package/dist/modules/access/kb-read-filter.d.ts +0 -41
  1271. package/dist/modules/access/kb-read-filter.d.ts.map +0 -1
  1272. package/dist/modules/access/kb-read-filter.js +0 -60
  1273. package/dist/modules/access/kb-read-filter.js.map +0 -1
  1274. package/dist/modules/access/render-roles-yaml.d.ts +0 -22
  1275. package/dist/modules/access/render-roles-yaml.d.ts.map +0 -1
  1276. package/dist/modules/access/render-roles-yaml.js +0 -56
  1277. package/dist/modules/access/render-roles-yaml.js.map +0 -1
  1278. package/dist/modules/access/roles-yaml-guard.d.ts +0 -56
  1279. package/dist/modules/access/roles-yaml-guard.d.ts.map +0 -1
  1280. package/dist/modules/access/roles-yaml-guard.js +0 -79
  1281. package/dist/modules/access/roles-yaml-guard.js.map +0 -1
  1282. package/dist/modules/diff/diff-paths.d.ts +0 -2
  1283. package/dist/modules/diff/diff-paths.d.ts.map +0 -1
  1284. package/dist/modules/diff/diff-paths.js +0 -9
  1285. package/dist/modules/diff/diff-paths.js.map +0 -1
  1286. package/dist/modules/groups/group-provision.service.d.ts +0 -130
  1287. package/dist/modules/groups/group-provision.service.d.ts.map +0 -1
  1288. package/dist/modules/groups/group-provision.service.js +0 -288
  1289. package/dist/modules/groups/group-provision.service.js.map +0 -1
  1290. package/dist/modules/groups/groups.contract.d.ts +0 -106
  1291. package/dist/modules/groups/groups.contract.d.ts.map +0 -1
  1292. package/dist/modules/groups/groups.contract.js +0 -36
  1293. package/dist/modules/groups/groups.contract.js.map +0 -1
  1294. package/dist/modules/groups/groups.routes.d.ts +0 -42
  1295. package/dist/modules/groups/groups.routes.d.ts.map +0 -1
  1296. package/dist/modules/groups/groups.routes.js +0 -379
  1297. package/dist/modules/groups/groups.routes.js.map +0 -1
  1298. package/dist/modules/groups/groups.service.d.ts +0 -60
  1299. package/dist/modules/groups/groups.service.d.ts.map +0 -1
  1300. package/dist/modules/groups/groups.service.js +0 -172
  1301. package/dist/modules/groups/groups.service.js.map +0 -1
  1302. package/dist/modules/groups/index.d.ts +0 -7
  1303. package/dist/modules/groups/index.d.ts.map +0 -1
  1304. package/dist/modules/groups/index.js +0 -6
  1305. package/dist/modules/groups/index.js.map +0 -1
  1306. package/dist/modules/groups/join-proposals.d.ts +0 -53
  1307. package/dist/modules/groups/join-proposals.d.ts.map +0 -1
  1308. package/dist/modules/groups/join-proposals.js +0 -67
  1309. package/dist/modules/groups/join-proposals.js.map +0 -1
  1310. package/dist/modules/groups/join-requests.service.d.ts +0 -81
  1311. package/dist/modules/groups/join-requests.service.d.ts.map +0 -1
  1312. package/dist/modules/groups/join-requests.service.js +0 -135
  1313. package/dist/modules/groups/join-requests.service.js.map +0 -1
  1314. package/dist/modules/mcp/mcp-session-store.d.ts +0 -74
  1315. package/dist/modules/mcp/mcp-session-store.d.ts.map +0 -1
  1316. package/dist/modules/mcp/mcp-session-store.js +0 -131
  1317. package/dist/modules/mcp/mcp-session-store.js.map +0 -1
  1318. package/dist/modules/workflow/file-change-notifier.d.ts +0 -38
  1319. package/dist/modules/workflow/file-change-notifier.d.ts.map +0 -1
  1320. package/dist/modules/workflow/file-change-notifier.js +0 -22
  1321. package/dist/modules/workflow/file-change-notifier.js.map +0 -1
  1322. package/dist/modules/workflow/git/branch-name.d.ts +0 -10
  1323. package/dist/modules/workflow/git/branch-name.d.ts.map +0 -1
  1324. package/dist/modules/workflow/git/branch-name.js +0 -76
  1325. package/dist/modules/workflow/git/branch-name.js.map +0 -1
  1326. package/dist/modules/workflow/git/clone-config.d.ts +0 -61
  1327. package/dist/modules/workflow/git/clone-config.d.ts.map +0 -1
  1328. package/dist/modules/workflow/git/clone-config.js +0 -69
  1329. package/dist/modules/workflow/git/clone-config.js.map +0 -1
  1330. package/dist/modules/workflow/git/mutex.d.ts +0 -11
  1331. package/dist/modules/workflow/git/mutex.d.ts.map +0 -1
  1332. package/dist/modules/workflow/git/mutex.js +0 -23
  1333. package/dist/modules/workflow/git/mutex.js.map +0 -1
  1334. package/dist/modules/workflow/locking-filesystem.d.ts +0 -137
  1335. package/dist/modules/workflow/locking-filesystem.d.ts.map +0 -1
  1336. package/dist/modules/workflow/locking-filesystem.js +0 -553
  1337. package/dist/modules/workflow/locking-filesystem.js.map +0 -1
  1338. package/dist/modules/workflow/read-only-filesystem.d.ts +0 -24
  1339. package/dist/modules/workflow/read-only-filesystem.d.ts.map +0 -1
  1340. package/dist/modules/workflow/read-only-filesystem.js +0 -39
  1341. package/dist/modules/workflow/read-only-filesystem.js.map +0 -1
  1342. package/dist/modules/workflow/workflow.errors.d.ts +0 -197
  1343. package/dist/modules/workflow/workflow.errors.d.ts.map +0 -1
  1344. package/dist/modules/workflow/workflow.errors.js +0 -298
  1345. package/dist/modules/workflow/workflow.errors.js.map +0 -1
  1346. package/dist/modules/workspace/bevel-ignore.d.ts.map +0 -1
  1347. package/dist/modules/workspace/bevel-ignore.js.map +0 -1
  1348. package/dist/modules/workspace/kb-seed.interface.d.ts +0 -36
  1349. package/dist/modules/workspace/kb-seed.interface.d.ts.map +0 -1
  1350. package/dist/modules/workspace/kb-seed.interface.js +0 -2
  1351. package/dist/modules/workspace/kb-seed.interface.js.map +0 -1
  1352. package/dist/modules/workspace/kb-seed.service.d.ts +0 -125
  1353. package/dist/modules/workspace/kb-seed.service.d.ts.map +0 -1
  1354. package/dist/modules/workspace/kb-seed.service.js +0 -535
  1355. package/dist/modules/workspace/kb-seed.service.js.map +0 -1
  1356. package/dist/modules/workspace/plugins-migration.d.ts +0 -50
  1357. package/dist/modules/workspace/plugins-migration.d.ts.map +0 -1
  1358. package/dist/modules/workspace/plugins-migration.js +0 -379
  1359. package/dist/modules/workspace/plugins-migration.js.map +0 -1
  1360. package/dist/shared/fs-errors.d.ts +0 -8
  1361. package/dist/shared/fs-errors.d.ts.map +0 -1
  1362. package/dist/shared/fs-errors.js +0 -11
  1363. package/dist/shared/fs-errors.js.map +0 -1
  1364. package/dist/shared/fs-walk.d.ts +0 -23
  1365. package/dist/shared/fs-walk.d.ts.map +0 -1
  1366. package/dist/shared/fs-walk.js +0 -42
  1367. package/dist/shared/fs-walk.js.map +0 -1
  1368. package/dist/shared/hash-email.d.ts +0 -11
  1369. package/dist/shared/hash-email.d.ts.map +0 -1
  1370. package/dist/shared/hash-email.js +0 -14
  1371. package/dist/shared/hash-email.js.map +0 -1
  1372. package/dist/shared/kb-walk.d.ts +0 -52
  1373. package/dist/shared/kb-walk.d.ts.map +0 -1
  1374. package/dist/shared/kb-walk.js +0 -76
  1375. package/dist/shared/kb-walk.js.map +0 -1
  1376. package/src/modules/diff/diff-paths.ts +0 -9
  1377. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1378. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1379. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1380. package/src/shared/__tests__/fs-walk.test.ts +0 -63
  1381. package/src/shared/__tests__/kb-walk.test.ts +0 -107
  1382. package/src/shared/fs-errors.ts +0 -10
  1383. package/src/shared/fs-walk.ts +0 -45
  1384. package/src/shared/hash-email.ts +0 -14
  1385. package/src/shared/kb-walk.ts +0 -95
@@ -1,24 +1,66 @@
1
1
  import fs from 'node:fs/promises';
2
+ import { logger } from '../../shared/logging.js';
3
+
4
+ const log = logger('workspace.routes');
2
5
  import path from 'node:path';
3
- import { IGNORE_FILENAME } from './bevel-ignore.js';
6
+ import { IGNORE_FILENAME, isAbsence, type ITreeWalker } from '../../shared/fs.contract.js';
7
+ import { printable } from '../../shared/printable.js';
4
8
  import type { IAdminAccessService } from '../admin/admin.interface.js';
5
9
  import express from 'express';
6
10
  import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared';
7
- import { DEFAULT_BRANCH, KNOWLEDGE_DIR, reservedRootDirNames } from '@bevel-software/platform-shared';
11
+ import {
12
+ DEFAULT_BRANCH,
13
+ KNOWLEDGE_DIR,
14
+ canonicalRelativePath,
15
+ folderPlaceholderPath,
16
+ isPlatformFile,
17
+ isPlatformRestoreShape,
18
+ platformFileCreationRefusal,
19
+ platformFileRefusal,
20
+ reservedRootDirNames,
21
+ } from '@bevel-software/platform-shared';
8
22
  import { FolderTooLargeError, type ReadTreeFilter } from './workspace.service.js';
9
23
  import { branchForWorkspaceId } from '../../shared/workspace-id.js';
10
- import type { WorkspaceService } from './workspace.service.js';
24
+ import { EntryExistsError, type WorkspaceService } from './workspace.service.js';
11
25
  import type { AuthService } from '../auth/auth.service.js';
12
26
  import type { IAccessControl } from '../access/access-control.interface.js';
13
27
  import { canReadWorkspacePath, resolveReadableMap, toKbRelative } from '../access-model/kb-read-filter.js';
14
28
  import type { ICreatorAccess } from '../access-model/creator.js';
29
+ import type { IChangeReadGate } from '../access-model/change-gate.js';
15
30
  import { isRolesYamlPath, assertRolesYamlParsable } from '../access-model/roles-yaml-guard.js';
16
31
  import type { WorkflowEventBus } from '../workflow/event-bus.js';
17
- import { WorkflowDomainError } from '../../shared/domain-errors.js';
32
+ import { PathTraversalError, WorkflowDomainError } from '../../shared/domain-errors.js';
33
+ import { domainErrorBody } from '../../shared/http-errors.js';
34
+ import { assertWithinDirectory } from '../../shared/path-containment.js';
35
+ import { normalizeWorkspacePath } from '../kb-fs/repo-path.js';
36
+ import { hasGitInternalsSegment } from '../../shared/git-internals.js';
37
+ import { createGitInternalsRouteGuard } from './git-internals.middleware.js';
38
+ import { removeEmptyDirs } from './empty-dirs.js';
18
39
  import '../auth/auth.middleware.js'; // Express Request augmentation
40
+ import type { SkillSaveCheck } from './workspace.tools.js';
19
41
 
20
42
  const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
21
43
 
44
+ /**
45
+ * One file identity from one request field, or `null` when the caller sent
46
+ * nothing usable.
47
+ *
48
+ * TYPED, not cast: a repeated `?path=a&path=b` arrives as an array and a JSON
49
+ * body can hold anything, so `as string` would hand a non-string to the
50
+ * canonicaliser and throw a 500 on what is a client mistake.
51
+ *
52
+ * CANONICAL, because the mutating verbs on this surface coordinate on the
53
+ * path: `PUT` takes an in-process write turn on it, all of them take the
54
+ * workflow lock row keyed by it, and the bytes live at it. A save spelled
55
+ * `x/a.md` and a delete spelled `./x//a.md` would otherwise take two
56
+ * different lock rows for one file and interleave. See
57
+ * `canonicalRelativePath` for what it refuses to touch.
58
+ */
59
+ function requestPath(value: unknown): string | null {
60
+ if (typeof value !== 'string' || value.length === 0) return null;
61
+ return canonicalRelativePath(value);
62
+ }
63
+
22
64
  /**
23
65
  * Workspaces are per-branch (PLAN §3). Any authenticated user can access
24
66
  * any branch's workspace; coordination of concurrent edits lives in the
@@ -43,8 +85,64 @@ export function createWorkspaceRoutes(
43
85
  kbDirName: string,
44
86
  creatorAccess: ICreatorAccess,
45
87
  adminAccess: IAdminAccessService,
88
+ disk: ITreeWalker,
89
+ /** Save-time skill check: `PUT /file` on a SKILL.md answers with `warnings` (advisory, never a refusal). */
90
+ skillSaveCheck?: SkillSaveCheck,
91
+ /**
92
+ * Read-before-write, asked ahead of the lock where a route puts bytes on
93
+ * disk BEFORE it locks them (archive extraction). Every other route's writes
94
+ * meet the same gate inside `acquireLock`. Optional so route harnesses that
95
+ * exercise other behaviour need not wire it.
96
+ */
97
+ changeGate?: IChangeReadGate,
46
98
  ): express.Router {
47
99
  const router = express.Router();
100
+ const gitInternalsRouteGuard = createGitInternalsRouteGuard(workspaceService);
101
+
102
+ // The git folder is refused ahead of every route here by
103
+ // `createGitInternalsRouteGuard`, which the app mounts once for the whole
104
+ // `/workspace/:id` prefix (see `git-internals.middleware.ts`). Mounted here
105
+ // too so this router carries the rule wherever it is mounted on its own.
106
+ router.use("/workspace/:id", gitInternalsRouteGuard);
107
+
108
+ /**
109
+ * The REPOSITORY path a request names, or `null` once the refusal has been
110
+ * sent.
111
+ *
112
+ * Every handler on this surface calls this FIRST, before the access check and
113
+ * before the service, so one path is what is checked and what is written.
114
+ * That was the hole: the access helpers stripped `<kbDirName>/` when it was
115
+ * there and otherwise took the path as written, so an unprefixed
116
+ * `KnowledgeBase/Reports` was judged as a repository path and then created
117
+ * beside the repository — a folder, eight documents and a `Plugins/` tree on
118
+ * core-staging, none of it ever committed. The normaliser closes it by making
119
+ * the unprefixed spelling MEAN the repository path it was judged as.
120
+ */
121
+ function inRepo(res: express.Response, wsPath: string): string | null {
122
+ try {
123
+ return normalizeWorkspacePath(wsPath, kbDirName);
124
+ } catch (err) {
125
+ sendError(res, err);
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * A repository path's absolute location, checked against the repository root.
132
+ *
133
+ * The ONE place this router resolves a workspace path — the delete fast-path,
134
+ * the platform-restore look and the folder-keep all used to call
135
+ * `path.resolve(workspaceDir, …)` for themselves, against the WORKSPACE dir,
136
+ * which is the containment an unprefixed path satisfied on its way to being
137
+ * written beside the checkout. A drift-guard test fails if a second one
138
+ * appears.
139
+ */
140
+ async function absoluteInRepo(workspaceId: string, repoRelative: string): Promise<string> {
141
+ const workspaceDir = await workspaceService.getWorkspacePath(workspaceId);
142
+ const absolute = path.resolve(workspaceDir, repoRelative);
143
+ assertWithinDirectory(absolute, path.join(workspaceDir, kbDirName));
144
+ return absolute;
145
+ }
48
146
 
49
147
  function authenticated(
50
148
  req: express.Request,
@@ -84,12 +182,49 @@ export function createWorkspaceRoutes(
84
182
  }
85
183
  return user;
86
184
  } catch (err) {
87
- console.error('[workspace.routes] requireUser failed:', err);
185
+ log.error('requireUser failed:', { err });
88
186
  res.status(500).json({ error: 'Internal server error' });
89
187
  return null;
90
188
  }
91
189
  }
92
190
 
191
+ /**
192
+ * Whether a failed op is one that provably touched no bytes, so its lock
193
+ * may be released with the disk and the commit queue left exactly as they
194
+ * are (`releaseLockUntouched`) rather than reset to HEAD.
195
+ *
196
+ * This is not tidiness. A release that discards resets the PATH, not "this
197
+ * request's changes" — git has no notion of the latter — so when the path
198
+ * holds a landed save whose commit is still queued (commits run out of band
199
+ * in the pending-commits worker), the discard destroys that save. Two moves
200
+ * racing onto the same free name are exactly that situation: the winner
201
+ * lands and enqueues, the loser then takes the same destination lock, is
202
+ * refused because the name is now taken, and its unwind would throw the
203
+ * winner's file away. One refusal, two lost files.
204
+ *
205
+ * A destination-taken refusal qualifies because nothing it can do writes:
206
+ * the preflight throws before the move is attempted, and the move's own
207
+ * no-clobber calls (`link`, `mkdir`, `open` with `O_EXCL` — see
208
+ * `shared/rename-no-replace.ts`) fail without creating anything, with the
209
+ * folder claim rolling itself back. `LockingFilesystem.withLock` draws the
210
+ * same line for its `CheckRefusal`, in the same words: a refusal that wrote
211
+ * nothing releases untouched.
212
+ *
213
+ * Deliberately a closed list of refusal TYPES rather than a guess at what
214
+ * an op did. An unrecognised failure keeps the discarding release, which is
215
+ * the fail-closed side: at worst it throws away bytes nobody promised to
216
+ * keep, where the other mistake throws away bytes someone was told were
217
+ * saved.
218
+ *
219
+ * One type, not two: the move's lower-level `DestinationTakenError` never
220
+ * reaches this layer — `moveEntry` converts it into `EntryExistsError`, the
221
+ * refusal this surface answers 409 with — so recognising it here as well
222
+ * would be a branch nothing can take.
223
+ */
224
+ function wroteNothing(err: unknown): boolean {
225
+ return err instanceof EntryExistsError;
226
+ }
227
+
93
228
  /**
94
229
  * Acquire the workflow lock for `(workspaceId, branch, targetPath)`, run
95
230
  * `op`, then release. Release commits + pushes the file as a one-file
@@ -117,7 +252,17 @@ export function createWorkspaceRoutes(
117
252
  * `skipPush` option is gone — under the queue model commits +
118
253
  * pushes happen out of band in the worker, not inline here.)
119
254
  */
120
- options?: { skipFsTreeEvent?: boolean },
255
+ options?: {
256
+ skipFsTreeEvent?: boolean;
257
+ /**
258
+ * This acquire is the destination side of an admin putting a misplaced
259
+ * platform file back, coming from `source`. Passed straight to
260
+ * `acquireLock`, which VERIFIES both halves — that the move is a restore
261
+ * at all, and that this caller may make it — rather than believing
262
+ * either. See the `IWorkflowService.acquireLock` contract.
263
+ */
264
+ platformRestore?: { source: string };
265
+ },
121
266
  ): Promise<T> {
122
267
  const branch = branchForWorkspaceId(workspaceId);
123
268
  // If the caller already holds the lock, do NOT acquire-and-release
@@ -153,7 +298,9 @@ export function createWorkspaceRoutes(
153
298
  eventBus.emit({ kind: 'fs-tree-changed', workspaceId, branch });
154
299
  return result;
155
300
  }
156
- const acquired = await workflowService.acquireLock(workspaceId, branch, targetPath, user);
301
+ const acquired = await workflowService.acquireLock(workspaceId, branch, targetPath, user, {
302
+ platformRestore: options?.platformRestore,
303
+ });
157
304
  if (!acquired.acquired) {
158
305
  const holder = acquired.lock.holderName || 'another user';
159
306
  const err: Error & { status?: number } = new Error(
@@ -162,16 +309,22 @@ export function createWorkspaceRoutes(
162
309
  err.status = 409;
163
310
  throw err;
164
311
  }
165
- // Two release modes, depending on whether the op succeeded:
312
+ // Three release modes, the same three `LockingFilesystem.withLock` uses,
313
+ // depending on what the op did:
314
+ //
315
+ // - op() FAILED having possibly WRITTEN → drop the lock WITHOUT
316
+ // enqueueing a commit. The op may have written partial bytes to disk
317
+ // before throwing (write that errored mid-stream, etc.). A normal
318
+ // `releaseLock` would enqueue a commit for whatever's on disk and the
319
+ // worker would silently persist that partial state as a real
320
+ // committed change. `releaseLockNoCommit` resets the path to HEAD, so
321
+ // partial bytes never become a committed change with the user's name
322
+ // on it.
166
323
  //
167
- // - op() FAILED → drop the lock WITHOUT enqueueing a commit. The
168
- // op may have written partial bytes to disk before throwing
169
- // (write that errored mid-stream, etc.). A normal `releaseLock`
170
- // would enqueue a commit for whatever's on disk and the worker
171
- // would silently persist that partial state as a real committed
172
- // change. `releaseLockNoCommit` drops the lock row only —
173
- // partial disk state stays where it is but never becomes a
174
- // committed change with the user's name on it.
324
+ // - op() FAILED having written NOTHING → `releaseLockUntouched`: drop
325
+ // the lock row and leave both the disk and the commit queue exactly
326
+ // as they are. See `wroteNothing` for which refusals qualify and why
327
+ // the distinction is load-bearing rather than tidy.
175
328
  //
176
329
  // - op() SUCCEEDED → release. The new releaseLock enqueues a
177
330
  // pending-commit row (the actual `commitFile + push` runs out
@@ -185,12 +338,17 @@ export function createWorkspaceRoutes(
185
338
  result = await op();
186
339
  opSucceeded = true;
187
340
  } catch (err) {
341
+ const untouched = wroteNothing(err);
188
342
  try {
189
- await workflowService.releaseLockNoCommit(workspaceId, branch, targetPath, user);
343
+ if (untouched) {
344
+ await workflowService.releaseLockUntouched(workspaceId, branch, targetPath, user);
345
+ } else {
346
+ await workflowService.releaseLockNoCommit(workspaceId, branch, targetPath, user);
347
+ }
190
348
  } catch (releaseErr) {
191
- console.warn(
192
- `[workspace.routes] releaseLockNoCommit failed after op error for "${targetPath}":`,
193
- releaseErr instanceof Error ? releaseErr.message : releaseErr,
349
+ log.warn(
350
+ `${untouched ? 'releaseLockUntouched' : 'releaseLockNoCommit'} failed after op error for "${targetPath}":`,
351
+ { err: releaseErr },
194
352
  );
195
353
  }
196
354
  throw err;
@@ -217,19 +375,65 @@ export function createWorkspaceRoutes(
217
375
  return result;
218
376
  }
219
377
 
378
+ /**
379
+ * Keep the folder a removal just emptied. A folder exists until it is
380
+ * deleted explicitly, so when deleting a file (or a subfolder), or moving
381
+ * one out, leaves its parent empty, the parent gets the placeholder in its
382
+ * own lock cycle — committed like any other save, within the same request.
383
+ * Only folders inside the repository qualify, never the clone folder
384
+ * itself.
385
+ *
386
+ * It runs in the folder's turn, which an explicit folder delete also takes,
387
+ * and looks again inside it: a folder that is gone by then was deleted
388
+ * explicitly and stays gone, and one that gained an entry needs nothing.
389
+ * The placeholder is only ever written into a folder that exists, and never
390
+ * through a link (see `writeFolderPlaceholder`).
391
+ *
392
+ * A failure is the request's failure: the removal landed, but a request
393
+ * that answers success would leave a folder that vanishes on the next
394
+ * clone, which is exactly what this rule forbids. It keeps its own status
395
+ * (a contended placeholder lock is still a 409) and only gains the context.
396
+ */
397
+ async function keepFolderOf(
398
+ workspaceId: string,
399
+ user: AuthUser,
400
+ removedPath: string,
401
+ ): Promise<void> {
402
+ const trimmed = removedPath.replace(/\/+$/, '');
403
+ const dir = trimmed.includes('/') ? trimmed.slice(0, trimmed.lastIndexOf('/')) : '';
404
+ if (!dir.startsWith(`${kbDirName}/`)) return;
405
+ try {
406
+ const absolute = await absoluteInRepo(workspaceId, dir);
407
+ await workspaceService.withFolderTurn(workspaceId, dir, async () => {
408
+ if (!(await isEmptyFolder(absolute))) return;
409
+ // One tree refresh per request: the removal already announced it.
410
+ await withLock(
411
+ workspaceId,
412
+ user,
413
+ folderPlaceholderPath(dir),
414
+ () => workspaceService.writeFolderPlaceholder(workspaceId, dir),
415
+ { skipFsTreeEvent: true },
416
+ );
417
+ });
418
+ } catch (err) {
419
+ const reason = err instanceof Error ? err.message : String(err);
420
+ log.error(`could not keep the folder ${printable(dir)} after removing ${printable(removedPath)}: ${printable(reason)}`);
421
+ throw withKeptFolderContext(err, `"${removedPath}" was removed, but its folder "${dir}" could not be kept: ${reason}`);
422
+ }
423
+ }
424
+
220
425
  /**
221
426
  * Map any thrown error to an HTTP response. Centralises the response
222
427
  * shape so each route handler stays focused on its own logic.
223
- * - `WorkflowDomainError` → its `.status` + `.payload`
224
- * - Path traversal → 403
225
- * - Invalid path / zip → 400 (Only .zip is also 400)
226
- * - Could not read zip → 422
428
+ * - `WorkflowDomainError` → its `.status` + `.payload` (a traversal is a
429
+ * 403, an invalid path a 400, an unreadable archive a 422 — each by
430
+ * its TYPE; this surface used to recognise all three by their message)
227
431
  * - Anything else with a `.status` → that status
228
432
  * - Default → 500
229
433
  */
230
434
  function sendError(res: express.Response, err: unknown): void {
231
435
  if (err instanceof WorkflowDomainError) {
232
- res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
436
+ res.status(err.status).json(domainErrorBody(err));
233
437
  return;
234
438
  }
235
439
  const msg = err instanceof Error ? err.message : 'Unknown error';
@@ -238,18 +442,6 @@ export function createWorkspaceRoutes(
238
442
  res.status(status).json({ error: msg });
239
443
  return;
240
444
  }
241
- if (msg === 'Path traversal detected') {
242
- res.status(403).json({ error: msg });
243
- return;
244
- }
245
- if (msg.startsWith('Invalid path') || msg.startsWith('Only .zip')) {
246
- res.status(400).json({ error: msg });
247
- return;
248
- }
249
- if (msg.startsWith('Could not read zip file')) {
250
- res.status(422).json({ error: msg });
251
- return;
252
- }
253
445
  res.status(500).json({ error: msg });
254
446
  }
255
447
 
@@ -292,10 +484,7 @@ export function createWorkspaceRoutes(
292
484
  // Log the full stack so the next 500 isn't a guessing game — the
293
485
  // bare `error.message` we returned before lost most diagnostic
294
486
  // signal (cause chain, stack frames, error class).
295
- console.error(
296
- `[workspace.routes] GET /workspace failed branch=${req.query.branch ?? '(default)'}:`,
297
- error instanceof Error ? error.stack ?? error.message : error,
298
- );
487
+ log.error(`GET /workspace failed branch=${req.query.branch ?? '(default)'}:`, { err: error });
299
488
  const msg = error instanceof Error ? error.message : 'Unknown error';
300
489
  res.status(500).json({ error: msg });
301
490
  }
@@ -388,12 +577,11 @@ export function createWorkspaceRoutes(
388
577
  let allowed: boolean;
389
578
  try {
390
579
  // `download` is a per-path verb in access.md, resolved via the same
391
- // chain walk as `write`. Strip the kbDirName prefix so the resolver
392
- // receives a repo-relative path (mirrors the lock-acquire gate's
393
- // strip in workflow.service.ts's assertCanWriteAtPath).
394
- const repoRelative = relativePath.startsWith(`${kbDirName}/`)
395
- ? relativePath.slice(kbDirName.length + 1)
396
- : relativePath;
580
+ // chain walk as `write`. The caller has already normalised the path into
581
+ // the repository, so this only takes the prefix off — where it used to
582
+ // pass an unprefixed path through untouched and judge a stray as if it
583
+ // were inside.
584
+ const repoRelative = toKbRelative(relativePath, kbDirName) ?? relativePath;
397
585
  allowed = await accessControl.canDownload(workspaceId, user.email, repoRelative);
398
586
  } catch (err) {
399
587
  sendError(res, err);
@@ -459,7 +647,7 @@ export function createWorkspaceRoutes(
459
647
  */
460
648
  function buildTreeReadFilter(workspaceId: string, userEmail: string): ReadTreeFilter {
461
649
  return async (wsRelPaths) => {
462
- const verdict = await resolveReadableMap(
650
+ const verdict: Map<string, boolean | 'unlisted'> = await resolveReadableMap(
463
651
  (w, e, rels) => accessControl.canReadBatch(w, e, rels),
464
652
  workspaceId,
465
653
  userEmail,
@@ -484,7 +672,7 @@ export function createWorkspaceRoutes(
484
672
  // `.bevelignore` is ADMIN-ONLY. It is the file that decides what the file
485
673
  // tree and the agent view show at all, so it is deployment configuration
486
674
  // rather than knowledge — and it sits alone in being visible: its
487
- // siblings (`.gitignore`, `roles.yaml`, `access.md`, `AGENTS.md`) are
675
+ // siblings (`.gitignore`, `roles.yaml`, `access.md`, the agent guide) are
488
676
  // already hidden from every reader by the shipped ignore rules. Somebody
489
677
  // who cannot act on it gains a puzzle; somebody who can needs to reach it.
490
678
  //
@@ -497,9 +685,13 @@ export function createWorkspaceRoutes(
497
685
  // on the default branch precisely so that editing `roles.yaml` on your
498
686
  // own branch cannot promote you, and the same reasoning applies to
499
687
  // anything gated on being an admin.
688
+ //
689
+ // `'unlisted'`, not `false`: hiding it withholds no content, so a
690
+ // non-admin in an empty knowledge base (every one ships this file) is
691
+ // told it is empty, not that something is being kept from them.
500
692
  const ignoreFiles = wsRelPaths.filter((wp) => path.basename(wp) === IGNORE_FILENAME);
501
693
  if (ignoreFiles.length > 0 && !(await adminAccess.isAdmin(userEmail))) {
502
- for (const wp of ignoreFiles) verdict.set(wp, false);
694
+ for (const wp of ignoreFiles) verdict.set(wp, 'unlisted');
503
695
  }
504
696
  return verdict;
505
697
  };
@@ -514,44 +706,70 @@ export function createWorkspaceRoutes(
514
706
  * grant on the same new directory survives. Runs BEFORE the creation itself
515
707
  * so the explorer never shows-then-hides the new subtree; if the creation
516
708
  * subsequently fails, the leftover is an empty new folder readable only by
517
- * its creator. Best-effort by contract: a failure here logs and never
518
- * blocks the creation.
709
+ * its creator.
710
+ *
711
+ * A failure here FAILS the creation, and propagates as it is. The seed is
712
+ * planned only for a new folder at a root the creator cannot read — the one
713
+ * creation the read gate lets past an unreadable spot — so without the
714
+ * grant the folder would come into existence invisible to the person who
715
+ * made it, which is exactly what the gate exists to prevent. The seed's
716
+ * own lock passes that gate too: if the folder appeared under someone else
717
+ * between the plan and this write, the refusal is theirs to see, and
718
+ * nothing lands.
519
719
  */
520
720
  async function seedCreatorAccessMd(
521
721
  workspaceId: string,
522
722
  user: AuthUser,
523
723
  plan: { wsRelPath: string; apply: (current: string) => string },
524
724
  ): Promise<void> {
525
- try {
526
- await withLock(workspaceId, user, plan.wsRelPath, async () => {
527
- let current = '';
528
- try {
529
- current = await workspaceService.readFile(workspaceId, plan.wsRelPath);
530
- } catch {
531
- // Not there yet — the normal case for a brand-new directory.
532
- }
533
- const next = plan.apply(current);
534
- if (next !== current) {
535
- await workspaceService.writeFile(workspaceId, plan.wsRelPath, next);
536
- }
537
- });
538
- creatorAccess.noteAccessFileWritten(workspaceId);
539
- } catch (err) {
540
- console.warn(
541
- `[workspace.routes] creator access.md seed failed for "${plan.wsRelPath}":`,
542
- err instanceof Error ? err.message : err,
543
- );
544
- }
725
+ await withLock(workspaceId, user, plan.wsRelPath, async () => {
726
+ let current = '';
727
+ try {
728
+ current = await workspaceService.readFile(workspaceId, plan.wsRelPath);
729
+ } catch {
730
+ // Not there yet — the normal case for a brand-new directory.
731
+ }
732
+ const next = plan.apply(current);
733
+ if (next !== current) {
734
+ await workspaceService.writeFile(workspaceId, plan.wsRelPath, next);
735
+ }
736
+ });
737
+ creatorAccess.noteAccessFileWritten(workspaceId);
545
738
  }
546
739
 
740
+ /**
741
+ * GET /workspace/:id/file/raw?path=<file>[&download=1][&v=<n>]
742
+ *
743
+ * The bytes of one workspace file: for the document renderers' fetches, the
744
+ * file tree's Download, and the `<img>` tags the markdown pipeline emits for
745
+ * `![alt](./assets/x.png)`. An `<img>` sends no Authorization header, so it
746
+ * is the auth middleware's `bevel_token` cookie fallback that lets a plain
747
+ * image tag through.
748
+ *
749
+ * request ──▶ auth: Bearer, else the cookie
750
+ * ──▶ read gate on the path 403 if the caller may not read it
751
+ * ──▶ download gate, if ?download=1 403 without the download: verb
752
+ * ──▶ readFileBinary 404 missing, 403 traversal
753
+ * ──▶ Content-Type from the extension, nosniff, a CSP sandbox for
754
+ * inline svg, attachment disposition for a download, and
755
+ * Cache-Control: private, no-cache
756
+ * ──▶ res.send(buffer): Express sets a weak ETag and answers 304 to
757
+ * a matching If-None-Match, so a revisited page costs a read
758
+ * and a hash per image rather than the bytes on the wire
759
+ *
760
+ * `?v=` is not read here. The frontend bumps it when it learns an image
761
+ * changed, so the browser asks for a URL it has not cached.
762
+ */
547
763
  router.get('/workspace/:id/file/raw', async (req, res) => {
548
764
  const id = authenticated(req, res);
549
765
  if (id === null) return;
550
- const filePath = req.query.path as string;
551
- if (!filePath) {
766
+ const requested = req.query.path;
767
+ if (typeof requested !== 'string' || requested.length === 0) {
552
768
  res.status(400).json({ error: 'path query parameter is required' });
553
769
  return;
554
770
  }
771
+ const filePath = inRepo(res, requested);
772
+ if (filePath === null) return;
555
773
  // `?download=1` flips this from inline-serve (used by PdfRenderer and
556
774
  // the image renderers) to "save to disk" — and the save path is gated
557
775
  // on per-path `download:` rules in access.md. The inline path stays
@@ -611,11 +829,18 @@ export function createWorkspaceRoutes(
611
829
  `attachment; filename*=UTF-8''${encodeURIComponent(basename)}`,
612
830
  );
613
831
  }
832
+ // `private`: this is one person's authenticated file, and a shared cache
833
+ // (a CDN, a corporate proxy) must not hand it to the next person.
834
+ // `no-cache`: the browser may keep it but asks before reusing it, which
835
+ // the ETag answers with a 304; a picture replaced under the same name is
836
+ // therefore never older than one revalidation.
837
+ res.setHeader('Cache-Control', 'private, no-cache');
614
838
  res.send(buffer);
615
839
  } catch (error) {
616
- const msg = error instanceof Error ? error.message : 'Unknown error';
617
- if (msg === 'Path traversal detected') {
618
- res.status(403).json({ error: msg });
840
+ // A traversal is the caller's 403; every other read failure here is the
841
+ // route's honest 404 (the image either is not there or cannot be shown).
842
+ if (error instanceof PathTraversalError) {
843
+ sendError(res, error);
619
844
  return;
620
845
  }
621
846
  res.status(404).json({ error: 'File not found' });
@@ -650,11 +875,13 @@ export function createWorkspaceRoutes(
650
875
  // Trim whitespace and strip trailing slashes — `foo/` would otherwise
651
876
  // basename to `''` and produce a misleading `.zip` filename. Done
652
877
  // before the empty check so `' / '` is also caught.
653
- const folderPath = rawPath.trim().replace(/\/+$/, '');
654
- if (!folderPath) {
878
+ const trimmedPath = rawPath.trim().replace(/\/+$/, '');
879
+ if (!trimmedPath) {
655
880
  res.status(400).json({ error: 'path query parameter is required' });
656
881
  return;
657
882
  }
883
+ const folderPath = inRepo(res, trimmedPath);
884
+ if (folderPath === null) return;
658
885
  if (req.query.download !== '1') {
659
886
  res.status(400).json({ error: 'download=1 is required for folder zip downloads' });
660
887
  return;
@@ -680,11 +907,11 @@ export function createWorkspaceRoutes(
680
907
  res.status(413).json({ error: error.message });
681
908
  return;
682
909
  }
683
- const msg = error instanceof Error ? error.message : 'Unknown error';
684
- if (msg === 'Path traversal detected') {
685
- res.status(403).json({ error: msg });
910
+ if (error instanceof PathTraversalError) {
911
+ sendError(res, error);
686
912
  return;
687
913
  }
914
+ const msg = error instanceof Error ? error.message : 'Unknown error';
688
915
  if (msg === 'Not a directory') {
689
916
  res.status(400).json({ error: msg });
690
917
  return;
@@ -697,11 +924,13 @@ export function createWorkspaceRoutes(
697
924
  router.get('/workspace/:id/file', async (req, res) => {
698
925
  const id = authenticated(req, res);
699
926
  if (id === null) return;
700
- const filePath = req.query.path as string;
701
- if (!filePath) {
927
+ const requested = requestPath(req.query.path);
928
+ if (requested === null) {
702
929
  res.status(400).json({ error: 'path query parameter is required' });
703
930
  return;
704
931
  }
932
+ const filePath = inRepo(res, requested);
933
+ if (filePath === null) return;
705
934
  if (!(await requireReadPermission(req, res, id, filePath))) return;
706
935
  try {
707
936
  const content = await workspaceService.readFile(id, filePath);
@@ -711,23 +940,46 @@ export function createWorkspaceRoutes(
711
940
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
712
941
  res.json({ content });
713
942
  } catch (error) {
714
- const msg = error instanceof Error ? error.message : 'Unknown error';
715
- if (msg === 'Path traversal detected') {
716
- res.status(403).json({ error: msg });
943
+ // 404 means ONE thing here: there is no file at this path. That is
944
+ // ENOENT, a parent that is not a directory (ENOTDIR), and a directory
945
+ // asked for as a file (EISDIR). Every other read failure keeps its own
946
+ // status, because callers act on the difference: the inline
947
+ // agent-description editor opens EMPTY on a 404 (a knowledge base older
948
+ // than the template has no such file yet), so dressing an unreadable
949
+ // file up as a missing one would offer an empty editor over content the
950
+ // save then overwrites.
951
+ if (isAbsence(error) || (error as NodeJS.ErrnoException).code === 'EISDIR') {
952
+ res.status(404).json({ error: 'File not found' });
717
953
  return;
718
954
  }
719
- res.status(404).json({ error: 'File not found' });
955
+ // Traversal stays a 403 and a malformed workspace id its domain status:
956
+ // both carry messages written to be read by the caller. (A traversal IS
957
+ // a domain error now, so the one check covers both.)
958
+ if (error instanceof WorkflowDomainError) {
959
+ sendError(res, error);
960
+ return;
961
+ }
962
+ // Anything else is a real failure, and it says so WITHOUT quoting
963
+ // itself: an errno message carries absolute workspace paths and a
964
+ // bootstrap failure carries git's stderr, neither of which belongs in a
965
+ // response body. The distinction this route exists to make is still
966
+ // made — an unreadable file is not a missing one — it is just not
967
+ // narrated to the client.
968
+ log.warn(`GET /file failed for "${filePath}" in "${id}":`, { err: error });
969
+ res.status(500).json({ error: "Couldn't read the file." });
720
970
  }
721
971
  });
722
972
 
723
973
  router.delete('/workspace/:id/file', async (req, res) => {
724
974
  const id = authenticated(req, res);
725
975
  if (id === null) return;
726
- const filePath = req.query.path as string;
727
- if (!filePath) {
976
+ const requested = requestPath(req.query.path);
977
+ if (requested === null) {
728
978
  res.status(400).json({ error: 'path query parameter is required' });
729
979
  return;
730
980
  }
981
+ const filePath = inRepo(res, requested);
982
+ if (filePath === null) return;
731
983
  const user = await requireUser(req, res);
732
984
  if (!user) return;
733
985
  try {
@@ -741,19 +993,12 @@ export function createWorkspaceRoutes(
741
993
  // emits are also coalesced into a single end-of-batch event so the
742
994
  // explorer doesn't refresh N times.
743
995
  const workspaceDir = await workspaceService.getWorkspacePath(id);
744
- const absolute = path.resolve(workspaceDir, filePath);
745
- // Boundary check before any `fs.*` call. The single-file path
746
- // bottoms out in `workspaceService.deleteFile` (which asserts the
747
- // boundary itself), but this fast-path resolves the absolute
748
- // ourselves and then `fs.stat` / `fs.rm` / `enumerateFilesUnder`
749
- // it. Without this guard, a `../escape` `filePath` would let the
750
- // dir-delete branch operate on directories outside the workspace.
751
- const workspaceRoot = path.resolve(workspaceDir);
752
- if (absolute !== workspaceRoot && !absolute.startsWith(workspaceRoot + path.sep)) {
753
- const err: Error & { status?: number } = new Error('Path traversal detected');
754
- err.status = 403;
755
- throw err;
756
- }
996
+ // Boundary check before any `fs.*` call — against the REPOSITORY root,
997
+ // which is where `absoluteInRepo` holds it. The single-file path bottoms
998
+ // out in `workspaceService.deleteFile` (which resolves inside the
999
+ // repository itself), but this fast-path needs the absolute for
1000
+ // `fs.stat` / `fs.rm` / `enumerateFilesUnder`.
1001
+ const absolute = await absoluteInRepo(id, filePath);
757
1002
  let stat: { isDirectory: () => boolean } | null = null;
758
1003
  try {
759
1004
  stat = await fs.stat(absolute);
@@ -761,56 +1006,77 @@ export function createWorkspaceRoutes(
761
1006
  // Not on disk — let workspaceService.deleteFile return its own 404.
762
1007
  }
763
1008
  if (stat?.isDirectory()) {
764
- const filesInDir = await enumerateFilesUnder(absolute, workspaceDir);
765
1009
  const branch = branchForWorkspaceId(id);
766
- for (const relFile of filesInDir) {
767
- await withLock(
768
- id,
769
- user,
770
- relFile,
771
- async () => {
772
- await workspaceService.deleteFile(id, relFile);
773
- },
774
- { skipFsTreeEvent: true },
775
- );
776
- }
777
- // No explicit push here — each per-file release enqueues a
778
- // pending-commits row, and the background worker drains them
779
- // (commit + push) on its own schedule. The N round-trips that
780
- // the old skipPush+pushBranch pattern collapsed into one happen
781
- // serially in the worker; user perception is unchanged because
782
- // the disk-side delete is what other sessions see via
783
- // `fs-tree-changed`.
784
- // Sweep the now-empty directory subtree off disk. Git doesn't track
785
- // empty folders, so there's nothing to commit; this is just disk
786
- // hygiene so the file tree stops showing the empty containers.
787
- //
788
- // This MUST recurse: a folder that held *subfolders* still has those
789
- // (now-empty) subdirectory shells on disk after the per-file deletes,
790
- // so a single non-recursive `rmdir(absolute)` would see a non-empty
791
- // dir and bail — leaving the folder visible in the tree and looking
792
- // undeletable (BEVA-132). `removeEmptyDirs` walks bottom-up and only
793
- // removes dirs that are *actually empty* at the moment it visits them,
794
- // so a concurrent writer's new file (and its parent chain) is
795
- // preserved — the same safety property the old non-recursive check had.
1010
+ // In the folder's turn: keeping a folder under this one (a file delete
1011
+ // racing this one) waits until the sweep is done, and then finds the
1012
+ // folder gone instead of writing it back.
1013
+ let filesInDir: string[];
796
1014
  try {
797
- await removeEmptyDirs(absolute);
798
- } catch (rmErr) {
799
- // Directory already gone (raced delete), or a concurrent writer
800
- // repopulated it. Either way, skip removal — the per-file deletes
801
- // are what's load-bearing.
802
- console.warn(
803
- `[workspace.routes] dir cleanup skipped for "${filePath}":`,
804
- rmErr instanceof Error ? rmErr.message : rmErr,
805
- );
1015
+ filesInDir = await workspaceService.withFolderTurn(id, filePath, async () => {
1016
+ const files = await enumerateFilesUnder(disk, absolute, workspaceDir);
1017
+ for (const relFile of files) {
1018
+ await withLock(
1019
+ id,
1020
+ user,
1021
+ relFile,
1022
+ async () => {
1023
+ await workspaceService.deleteFile(id, relFile);
1024
+ },
1025
+ { skipFsTreeEvent: true },
1026
+ );
1027
+ }
1028
+ // No explicit push here — each per-file release enqueues a
1029
+ // pending-commits row, and the background worker drains them
1030
+ // (commit + push) on its own schedule. The N round-trips that
1031
+ // the old skipPush+pushBranch pattern collapsed into one happen
1032
+ // serially in the worker; user perception is unchanged because
1033
+ // the disk-side delete is what other sessions see via
1034
+ // `fs-tree-changed`.
1035
+ // This is the EXPLICIT folder delete — the one operation that removes
1036
+ // a folder — so the walk above deleted the placeholders too, and the
1037
+ // now-empty directory subtree is swept off disk. Git doesn't track
1038
+ // empty folders, so there's nothing more to commit; this is disk
1039
+ // hygiene so the file tree stops showing the deleted containers.
1040
+ //
1041
+ // This MUST recurse: a folder that held *subfolders* still has those
1042
+ // (now-empty) subdirectory shells on disk after the per-file deletes,
1043
+ // so a single non-recursive `rmdir(absolute)` would see a non-empty
1044
+ // dir and bail — leaving the folder visible in the tree and looking
1045
+ // undeletable (BEVA-132). `removeEmptyDirs` walks bottom-up and only
1046
+ // removes dirs that are *actually empty* at the moment it visits them,
1047
+ // so a concurrent writer's new file (and its parent chain) is
1048
+ // preserved — the same safety property the old non-recursive check had.
1049
+ // A folder that vanished or filled up under a concurrent writer
1050
+ // is passed over inside the sweep; what reaches here is a real
1051
+ // failure (permissions, I/O) that left the folder on disk, and
1052
+ // the request fails — as the agent's delete does — rather than
1053
+ // the response reporting gone a folder the tree still shows. The
1054
+ // per-file deletes have landed either way, and the next delete
1055
+ // of the now-empty folder either sweeps it or says why not. The
1056
+ // cause is logged; the answer carries no path of the host's.
1057
+ try {
1058
+ await removeEmptyDirs(absolute);
1059
+ } catch (rmErr) {
1060
+ log.warn(`dir cleanup failed for ${printable(filePath)}`, { err: rmErr });
1061
+ throw new Error('The files were deleted, but the empty folder could not be removed. Delete it again.');
1062
+ }
1063
+ return files;
1064
+ });
1065
+ // The folder that HELD the deleted one was not asked to go.
1066
+ await keepFolderOf(id, user, filePath);
1067
+ } finally {
1068
+ // Single tree-refresh signal for the whole batch (we suppressed
1069
+ // the per-file ones via `skipFsTreeEvent`) — sent on failure too:
1070
+ // a batch that stops part way, or a parent that could not be kept,
1071
+ // has still changed the tree.
1072
+ eventBus.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
806
1073
  }
807
- // Single tree-refresh signal for the whole batch (we suppressed
808
- // the per-file ones via `skipFsTreeEvent`).
809
- eventBus.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
810
1074
  res.json({ status: 'deleted', count: filesInDir.length });
811
1075
  return;
812
1076
  }
813
1077
  await withLock(id, user, filePath, () => workspaceService.deleteFile(id, filePath));
1078
+ // Deleting content is not deleting structure: an emptied folder stays.
1079
+ await keepFolderOf(id, user, filePath);
814
1080
  res.json({ status: 'deleted' });
815
1081
  } catch (err) {
816
1082
  sendError(res, err);
@@ -820,14 +1086,69 @@ export function createWorkspaceRoutes(
820
1086
  router.patch('/workspace/:id/file', async (req, res) => {
821
1087
  const id = authenticated(req, res);
822
1088
  if (id === null) return;
823
- const { oldPath, newPath } = req.body as { oldPath?: string; newPath?: string };
824
- if (!oldPath || !newPath) {
1089
+ const body = (req.body ?? {}) as { oldPath?: unknown; newPath?: unknown };
1090
+ const requestedOld = requestPath(body.oldPath);
1091
+ const requestedNew = requestPath(body.newPath);
1092
+ if (requestedOld === null || requestedNew === null) {
825
1093
  res.status(400).json({ error: 'oldPath and newPath are required in body' });
826
1094
  return;
827
1095
  }
1096
+ // Both ends inside the repository before anything is judged: the platform-file
1097
+ // rules, the locks and the rename all read the same two paths.
1098
+ const oldPath = inRepo(res, requestedOld);
1099
+ if (oldPath === null) return;
1100
+ const newPath = inRepo(res, requestedNew);
1101
+ if (newPath === null) return;
1102
+ // Two spellings of one file are now ONE path, so a move can arrive with
1103
+ // both ends equal. `withLock` below would survive it — the inner
1104
+ // acquisition sees the lock the outer just took, held by this same user,
1105
+ // and runs straight through — but the move itself is a rename onto
1106
+ // itself that commits a change and tells the diff service the path was
1107
+ // both deleted and rewritten. It is a client mistake, so it is a 400.
1108
+ if (oldPath === newPath) {
1109
+ res.status(400).json({ error: 'oldPath and newPath must differ' });
1110
+ return;
1111
+ }
828
1112
  const user = await requireUser(req, res);
829
1113
  if (!user) return;
830
1114
  try {
1115
+ // A platform file stays in the folder the platform reads it from —
1116
+ // rename, move and drag all arrive here, and all three are refused.
1117
+ // Moving one out is not a choice to confirm: once the root has no
1118
+ // `access.md`, write on the root denies everyone and the move that
1119
+ // would undo it is the move the gate refuses.
1120
+ //
1121
+ // The single exception is that repair: an admin putting a misplaced
1122
+ // copy BACK. `isPlatformRestoreShape` says whether the MOVE is that
1123
+ // repair — it is judged on the source's name and the destination, not
1124
+ // on the source being a platform file where it currently sits, because
1125
+ // a stray `roles.yaml` in a folder is ordinary content there and is
1126
+ // still the copy the root is missing. `canRestorePlatformFile` then
1127
+ // decides who may make it, and whether the disk agrees.
1128
+ const oldRel = toKbRelative(oldPath, kbDirName);
1129
+ const newRel = toKbRelative(newPath, kbDirName);
1130
+ let platformRestore = false;
1131
+ if (oldRel !== null && newRel !== null && isPlatformRestoreShape(oldRel, newRel)) {
1132
+ platformRestore = await accessControl.canRestorePlatformFile(id, user.email, newRel);
1133
+ }
1134
+ // A platform file stays where the platform reads it …
1135
+ if (oldRel !== null && isPlatformFile(oldRel) && !platformRestore) {
1136
+ res.status(409).json({ error: platformFileRefusal(oldRel) });
1137
+ return;
1138
+ }
1139
+ // … and nothing else becomes one. `moveEntry` is a plain rename, so
1140
+ // without this a note renamed to `access.md` would come back as the
1141
+ // folder's rules, and a note dragged ONTO the root's `access.md` would
1142
+ // replace the rules that are there — neither of which the rule above,
1143
+ // which reads only the SOURCE, says anything about. The agent's move
1144
+ // tool has refused both since it shipped; this is the same sentence,
1145
+ // from the same place. Only the restore lands on a platform path, and
1146
+ // only where the file is missing (`canRestorePlatformFile` checked the
1147
+ // disk; the destination lock below checks it again, under the lock).
1148
+ if (newRel !== null && isPlatformFile(newRel) && !platformRestore) {
1149
+ res.status(409).json({ error: platformFileCreationRefusal(newRel) });
1150
+ return;
1151
+ }
831
1152
  // Move = rename on disk + commit on both sides. We lock-and-release
832
1153
  // the destination first (commits the new file's appearance), then
833
1154
  // lock-and-release the source path (commits its deletion). Two
@@ -848,11 +1169,46 @@ export function createWorkspaceRoutes(
848
1169
  // not a single merge-style rename commit, but git's log/blame
849
1170
  // rename detection still groups them visually after the fact.
850
1171
  const [firstLock, secondLock] = oldPath < newPath ? [oldPath, newPath] : [newPath, oldPath];
851
- await withLock(id, user, firstLock, () =>
852
- withLock(id, user, secondLock, () =>
853
- workspaceService.moveEntry(id, oldPath, newPath),
854
- ),
1172
+ // Only the DESTINATION side carries the restore claim. The source is an
1173
+ // ordinary write the caller must already hold: the exception exists so a
1174
+ // file can land where the platform reads it, not so an admin can take
1175
+ // one out of a folder that denies them.
1176
+ const restoreAt = (p: string) => ({
1177
+ platformRestore: platformRestore && p === newPath ? { source: oldPath } : undefined,
1178
+ });
1179
+ // The restore was authorised against a destination that was missing when
1180
+ // the access module looked. Both locks are in hand by the time this runs,
1181
+ // so nothing else can take that path from under the rename — but between
1182
+ // the two a writer still could, and a restore that lands on a platform
1183
+ // file replaces the rules it came to bring back. So the last thing the
1184
+ // move does before renaming is look again, inside the window it holds.
1185
+ const move = async () => {
1186
+ if (platformRestore) {
1187
+ const absoluteNew = await absoluteInRepo(id, newPath);
1188
+ const taken = await fs.stat(absoluteNew).then(
1189
+ () => true,
1190
+ // Genuine absence is the only "free": anything else is not an
1191
+ // answer, and an unanswered question does not clear the way onto
1192
+ // a platform file.
1193
+ (err: unknown) => !isAbsence(err),
1194
+ );
1195
+ if (taken) {
1196
+ const err: Error & { status?: number } = new Error(platformFileRefusal(newPath));
1197
+ err.status = 409;
1198
+ throw err;
1199
+ }
1200
+ }
1201
+ return workspaceService.moveEntry(id, oldPath, newPath);
1202
+ };
1203
+ await withLock(
1204
+ id,
1205
+ user,
1206
+ firstLock,
1207
+ () => withLock(id, user, secondLock, move, restoreAt(secondLock)),
1208
+ restoreAt(firstLock),
855
1209
  );
1210
+ // Moving the last entry out leaves its folder in place, like a delete.
1211
+ await keepFolderOf(id, user, oldPath);
856
1212
  res.json({ status: 'moved' });
857
1213
  } catch (err) {
858
1214
  sendError(res, err);
@@ -862,40 +1218,89 @@ export function createWorkspaceRoutes(
862
1218
  router.put('/workspace/:id/file', async (req, res) => {
863
1219
  const id = authenticated(req, res);
864
1220
  if (id === null) return;
865
- const filePath = req.query.path as string;
866
- if (!filePath) {
1221
+ // ONE spelling of the target from here down. `x/a.md`, `./x/a.md` and
1222
+ // `x//a.md` are the same file and all pass the path validator, and this
1223
+ // route coordinates on that path three times over: the in-process write
1224
+ // turn, the workflow lock row, and the bytes themselves. Two clients
1225
+ // spelling one file differently would otherwise take two different locks
1226
+ // and both pass their own precondition.
1227
+ const requested = requestPath(req.query.path);
1228
+ if (requested === null) {
867
1229
  res.status(400).json({ error: 'path query parameter is required' });
868
1230
  return;
869
1231
  }
870
- const { content, ifAbsent } = req.body as { content?: string; ifAbsent?: boolean };
1232
+ const filePath = inRepo(res, requested);
1233
+ if (filePath === null) return;
1234
+ const { content, ifAbsent, ifMatch } = req.body as {
1235
+ content?: string;
1236
+ ifAbsent?: boolean;
1237
+ ifMatch?: unknown;
1238
+ };
871
1239
  if (content === undefined) {
872
1240
  res.status(400).json({ error: 'content is required in body' });
873
1241
  return;
874
1242
  }
1243
+ if (ifMatch !== undefined && typeof ifMatch !== 'string') {
1244
+ res.status(400).json({ error: 'ifMatch, if provided, must be a string' });
1245
+ return;
1246
+ }
875
1247
  const user = await requireUser(req, res);
876
1248
  if (!user) return;
1249
+ // A precondition ANSWERS A QUESTION ABOUT CONTENT, so it is gated on
1250
+ // READING, not writing. Write authorisation happens at `acquireLock`
1251
+ // inside `withLock` below, which is after the compare: without this gate
1252
+ // the 409-versus-403 difference is a content-equality oracle on a file the
1253
+ // caller may not read, and the confirming case is a write. Same gate and
1254
+ // same 403 as `GET /file`; a caller who may write but not read can still
1255
+ // save, just not ask questions about what is there.
1256
+ if (ifMatch !== undefined && !(await requireReadPermission(req, res, id, filePath))) return;
877
1257
  try {
878
1258
  // Refuse a hand-edit that would leave roles.yaml unparseable BEFORE any
879
1259
  // byte hits disk — a broken roles.yaml is an app-wide admin lockout
880
- // (loadModel hard-throws). The dedicated Roles & Members surface has its
1260
+ // (loadModel hard-throws). The dedicated App roles surface has its
881
1261
  // own validate gate; this covers the raw-text editor path.
882
1262
  if (isRolesYamlPath(filePath, kbDirName)) assertRolesYamlParsable(content);
883
- // Creator read grant: a brand-new file at a spot whose access chain
884
- // doesn't grant the creator `read` would vanish from their own explorer
885
- // (read is default-deny). Plan BEFORE the write: a new subtree gets its
886
- // access.md seeded first; a loose .md carries the grant in its own
887
- // frontmatter as part of this same single write.
888
- const plan = await creatorAccess.planForCreate(id, user, filePath, 'file');
889
- if (plan?.kind === 'seed-access-md') await seedCreatorAccessMd(id, user, plan);
890
- const toWrite = plan?.kind === 'frontmatter' ? plan.apply(content) : content;
891
- // `ifAbsent` = exclusive create: the service's `wx` write turns a
892
- // concurrent or stale create against an existing file into a 409
893
- // instead of a silent replace. `withLock`'s failure arm releases
894
- // without committing, so the refusal leaves no trace.
895
- await withLock(id, user, filePath, () =>
896
- workspaceService.writeFile(id, filePath, toWrite, { failIfExists: ifAbsent === true }),
897
- );
898
- res.json({ status: 'written' });
1263
+ // One turn for the whole sequence, because the DECISION spans three
1264
+ // steps that must agree about the same file: check the precondition,
1265
+ // plan a creator-access grant that COMMITS, then write. A save landing
1266
+ // in between would leave that grant behind for a write about to be
1267
+ // refused, so the check is made here, before the plan, and nothing else
1268
+ // in this process may touch the path until the write is done. The write
1269
+ // inside takes the same turn re-entrantly.
1270
+ //
1271
+ // Across instances the workflow lock below is still the coordinator;
1272
+ // this only orders what this server is doing.
1273
+ await workspaceService.withPathTurn(id, filePath, async () => {
1274
+ // A stale `ifMatch` is refused before anything commits.
1275
+ if (ifMatch !== undefined) await workspaceService.assertContentMatches(id, filePath, ifMatch);
1276
+ // Creator read grant: a file that starts a new folder at a root the
1277
+ // creator cannot read would vanish from their own explorer (read is
1278
+ // default-deny). Plan BEFORE the write: the new folder gets its
1279
+ // access.md seeded first. Anywhere else the lock's read gate has
1280
+ // already decided — a creation the creator could not see is refused,
1281
+ // and one they can see needs no grant.
1282
+ const plan = await creatorAccess.planForCreate(id, user, filePath, 'file');
1283
+ if (plan?.kind === 'seed-access-md') await seedCreatorAccessMd(id, user, plan);
1284
+ const toWrite = content;
1285
+ // `ifAbsent` = exclusive create: the service's `wx` write turns a
1286
+ // concurrent or stale create against an existing file into a 409
1287
+ // instead of a silent replace. `withLock`'s failure arm releases
1288
+ // without committing, so the refusal leaves no trace.
1289
+ //
1290
+ // `ifMatch` = conditional write, the same 409 for a stale UPDATE: the
1291
+ // file must still hold the text the caller last read, re-checked at
1292
+ // the write itself as the decision that counts.
1293
+ await withLock(id, user, filePath, () =>
1294
+ workspaceService.writeFile(id, filePath, toWrite, {
1295
+ failIfExists: ifAbsent === true,
1296
+ expectedContent: ifMatch,
1297
+ }),
1298
+ );
1299
+ });
1300
+ // After the write, and only ever advisory: the check reports what the
1301
+ // saved skill names, it has no say in whether it saved.
1302
+ const warnings = skillSaveCheck ? await skillSaveCheck.checkSave(user.email, filePath, content) : [];
1303
+ res.json({ status: 'written', ...(warnings.length > 0 ? { warnings } : {}) });
899
1304
  } catch (err) {
900
1305
  sendError(res, err);
901
1306
  }
@@ -904,11 +1309,17 @@ export function createWorkspaceRoutes(
904
1309
  router.post('/workspace/:id/directory', async (req, res) => {
905
1310
  const id = authenticated(req, res);
906
1311
  if (id === null) return;
907
- const { path: dirPath, defer } = req.body as { path?: string; defer?: boolean };
908
- if (!dirPath) {
1312
+ const { path: requested, defer } = req.body as { path?: string; defer?: boolean };
1313
+ const canonical = requestPath(requested);
1314
+ if (canonical === null) {
909
1315
  res.status(400).json({ error: 'path is required in body' });
910
1316
  return;
911
1317
  }
1318
+ // The folder this call creates, as a repository path. `KnowledgeBase/Reports`
1319
+ // is `<kbDirName>/KnowledgeBase/Reports` — the access plan below, the
1320
+ // `.gitkeep` lock and the mkdir all use this one spelling.
1321
+ const dirPath = inRepo(res, canonical);
1322
+ if (dirPath === null) return;
912
1323
  const user = await requireUser(req, res);
913
1324
  if (!user) return;
914
1325
  try {
@@ -951,19 +1362,45 @@ export function createWorkspaceRoutes(
951
1362
  const id = authenticated(req, res);
952
1363
  if (id === null) return;
953
1364
  const body = (req.body ?? {}) as Record<string, unknown>;
954
- const zipPath = body.path;
955
- const destination = body.destination;
956
- if (typeof zipPath !== 'string' || zipPath.length === 0) {
1365
+ const requestedZip = body.path;
1366
+ const requestedDest = body.destination;
1367
+ if (typeof requestedZip !== 'string' || requestedZip.length === 0) {
957
1368
  res.status(400).json({ error: 'path is required in body and must be a string' });
958
1369
  return;
959
1370
  }
960
- if (destination !== undefined && (typeof destination !== 'string' || destination.length === 0)) {
1371
+ if (requestedDest !== undefined && (typeof requestedDest !== 'string' || requestedDest.length === 0)) {
961
1372
  res.status(400).json({ error: 'destination, if provided, must be a non-empty string' });
962
1373
  return;
963
1374
  }
1375
+ const zipPath = inRepo(res, requestedZip);
1376
+ if (zipPath === null) return;
1377
+ let destination: string | undefined;
1378
+ if (requestedDest !== undefined) {
1379
+ const normalizedDest = inRepo(res, requestedDest);
1380
+ if (normalizedDest === null) return;
1381
+ destination = normalizedDest;
1382
+ }
964
1383
  const user = await requireUser(req, res);
965
1384
  if (!user) return;
966
1385
  try {
1386
+ // The destination is asked about BEFORE anything is extracted: this is
1387
+ // the one route whose bytes land on disk ahead of the lock, so the
1388
+ // lock's read gate would find them already there. Same verdict, same
1389
+ // refusal, one step earlier. A destination inside a folder the caller
1390
+ // cannot read is refused whole; a folder they can read — or a new
1391
+ // folder directly under a root — extracts, and each file then meets
1392
+ // the lock as any other write does.
1393
+ //
1394
+ // A new root folder gets its creator's access.md seeded first, as
1395
+ // every other creation route does: extraction would otherwise bring
1396
+ // the folder into existence itself, and the first file's lock would
1397
+ // then find an existing folder the caller cannot read.
1398
+ // Both spellings are already repository paths, so the inferred parent is
1399
+ // one too — the same destination the service resolves.
1400
+ const destDir = destination ?? path.posix.dirname(zipPath);
1401
+ const plan = await creatorAccess.planForCreate(id, user, destDir, 'dir');
1402
+ if (plan?.kind === 'seed-access-md') await seedCreatorAccessMd(id, user, plan);
1403
+ await changeGate?.assertMayChange(id, user.email, destDir, 'dir');
967
1404
  // Extract first (all files land on disk), then sweep each extracted
968
1405
  // file through a lock+release so it commits + pushes as its own
969
1406
  // one-file change. Per-file commits mean the validator runs N times
@@ -971,27 +1408,18 @@ export function createWorkspaceRoutes(
971
1408
  // but correct for a 100-file zip. If a single file's release fails
972
1409
  // (e.g. validator 422), the loop stops there so the user sees the
973
1410
  // first concrete problem rather than a list of N similar failures.
974
- const result = await workspaceService.unzipFile(id, zipPath, destination);
1411
+ //
1412
+ // Each entry is asked about before it is written, through the same
1413
+ // gate: an archive can carry a path into a nested folder the caller
1414
+ // cannot read, which the destination check above cannot see. Such an
1415
+ // entry is skipped and reported, never written and then refused.
1416
+ const result = await workspaceService.unzipFile(id, zipPath, destination, async (entryPath) => {
1417
+ await changeGate?.assertMayChange(id, user.email, entryPath, 'file');
1418
+ });
975
1419
  for (const relFile of result.extracted) {
976
- await withLock(id, user, relFile, async () => {
977
- // The file is already on disk from the unzip; the release commits +
978
- // pushes it. If the extraction landed a markdown node the creator
979
- // can't read (default-deny chains), splice their read grant into
980
- // its frontmatter first so the committed change already carries it.
981
- // Best-effort: a failed grant write must not throw — withLock's
982
- // failure path releases WITHOUT committing and would discard the
983
- // extracted file from disk, turning a cosmetic grant failure into
984
- // data loss.
985
- try {
986
- const granted = await creatorAccess.grantInExtractedFile(id, user, relFile);
987
- if (granted !== null) await workspaceService.writeFile(id, relFile, granted);
988
- } catch (err) {
989
- console.warn(
990
- `[workspace.routes] creator grant on extracted "${relFile}" failed:`,
991
- err instanceof Error ? err.message : err,
992
- );
993
- }
994
- });
1420
+ // The file is already on disk from the unzip; the release commits +
1421
+ // pushes it.
1422
+ await withLock(id, user, relFile, async () => undefined);
995
1423
  }
996
1424
  res.json(result);
997
1425
  } catch (err) {
@@ -1002,11 +1430,16 @@ export function createWorkspaceRoutes(
1002
1430
  router.post('/workspace/:id/upload', async (req, res) => {
1003
1431
  const id = authenticated(req, res);
1004
1432
  if (id === null) return;
1005
- const filePath = req.query.path as string;
1006
- if (!filePath) {
1433
+ const requested = requestPath(req.query.path);
1434
+ if (requested === null) {
1007
1435
  res.status(400).json({ error: 'path query parameter is required' });
1008
1436
  return;
1009
1437
  }
1438
+ // An upload with no folder lands at the repository ROOT inside the
1439
+ // checkout, not beside it — eight staging test documents sat beside it
1440
+ // because this route took the path as written.
1441
+ const filePath = inRepo(res, requested);
1442
+ if (filePath === null) return;
1010
1443
  // `?defer=true` is the bulk-upload caller's signal to suppress the
1011
1444
  // per-file `fs-tree-changed` SSE — the caller emits one combined
1012
1445
  // refresh at end-of-burst via `POST /flush`. Pushing isn't deferred
@@ -1034,21 +1467,17 @@ export function createWorkspaceRoutes(
1034
1467
  chunks.push(buf);
1035
1468
  }
1036
1469
  const data = Buffer.concat(chunks);
1037
- // Creator read grant, mirroring PUT /file: seed a new subtree's
1038
- // access.md first, or fold the grant into an uploaded markdown file's
1039
- // frontmatter. Binary uploads into a pre-existing unreadable folder
1040
- // can't carry a per-file grant (no frontmatter) and proceed ungranted.
1470
+ // Creator read grant, mirroring PUT /file: an upload that starts a new
1471
+ // folder at a root seeds that folder's access.md first. An upload into
1472
+ // a folder the caller cannot read never gets this far — the lock's read
1473
+ // gate refuses it.
1041
1474
  const plan = await creatorAccess.planForCreate(id, user, filePath, 'file');
1042
1475
  if (plan?.kind === 'seed-access-md') await seedCreatorAccessMd(id, user, plan);
1043
- const toWrite =
1044
- plan?.kind === 'frontmatter'
1045
- ? Buffer.from(plan.apply(data.toString('utf8')), 'utf8')
1046
- : data;
1047
1476
  await withLock(
1048
1477
  id,
1049
1478
  user,
1050
1479
  filePath,
1051
- () => workspaceService.writeFileBinary(id, filePath, toWrite),
1480
+ () => workspaceService.writeFileBinary(id, filePath, data),
1052
1481
  defer ? { skipFsTreeEvent: true } : undefined,
1053
1482
  );
1054
1483
  res.json({ status: 'uploaded' });
@@ -1063,6 +1492,8 @@ export function createWorkspaceRoutes(
1063
1492
  // happens out of band in the worker, so this route's only remaining
1064
1493
  // responsibility is to emit the single combined `fs-tree-changed`
1065
1494
  // event the bulk caller suppressed per file.
1495
+ // It takes NO path: there is nothing for the normaliser to place, which is
1496
+ // why this route is the one on the surface that does not call it.
1066
1497
  router.post('/workspace/:id/flush', async (req, res) => {
1067
1498
  const id = authenticated(req, res);
1068
1499
  if (id === null) return;
@@ -1086,62 +1517,61 @@ export function createWorkspaceRoutes(
1086
1517
  * a recursive directory delete into per-file lock+release cycles so each
1087
1518
  * deletion lands as its own one-file change.
1088
1519
  *
1089
- * Skips `.git` to avoid trying to commit the internal git index when a
1090
- * caller targets it accidentally. Returns paths in stable lexical order
1091
- * for predictable commit sequencing.
1520
+ * Skips the git folder, in any spelling (`hasGitInternalsSegment`), so a
1521
+ * folder delete never enumerates — or deletes — the repository's git data. Returns paths in the walk's order — stable
1522
+ * and lexical — for predictable commit sequencing. A link counts as a file:
1523
+ * it is deleted as one, never followed. A folder that cannot be listed is
1524
+ * the delete's error: an enumeration with a hole in it would delete what it
1525
+ * saw and report success over what it did not.
1092
1526
  */
1093
- async function enumerateFilesUnder(absoluteDir: string, workspaceDir: string): Promise<string[]> {
1527
+ async function enumerateFilesUnder(disk: ITreeWalker, absoluteDir: string, workspaceDir: string): Promise<string[]> {
1094
1528
  const out: string[] = [];
1095
- async function walk(dir: string): Promise<void> {
1096
- let entries: import('node:fs').Dirent[];
1097
- try {
1098
- entries = await fs.readdir(dir, { withFileTypes: true });
1099
- } catch {
1100
- return;
1101
- }
1102
- entries.sort((a, b) => a.name.localeCompare(b.name));
1103
- for (const entry of entries) {
1104
- if (entry.name === '.git' && entry.isDirectory()) continue;
1105
- const child = path.join(dir, entry.name);
1106
- if (entry.isDirectory()) {
1107
- await walk(child);
1108
- } else {
1109
- out.push(path.relative(workspaceDir, child).replace(/\\/g, '/'));
1110
- }
1111
- }
1529
+ const relOf = (dir: string, name: string) =>
1530
+ path.relative(workspaceDir, path.join(absoluteDir, dir, name)).replace(/\\/g, '/');
1531
+ try {
1532
+ await disk.walk(absoluteDir, { skip: (e) => hasGitInternalsSegment(e.name), unreadable: 'throw' }, [
1533
+ {
1534
+ onFile: (dir, name) => void out.push(relOf(dir, name)),
1535
+ onOther: (dir, e) => void out.push(relOf(dir, e.name)),
1536
+ },
1537
+ ]);
1538
+ } catch (err) {
1539
+ // The operator's line carries the errno and the path; the caller's answer
1540
+ // names only the folder they asked about — an OS message would leak the
1541
+ // server's spelling of the workspace, and would not help them anyway.
1542
+ // Both halves of the log line are user- or disk-controlled text, so both
1543
+ // go through `printable`: a folder name or an OS message carrying a
1544
+ // control character must not steer the terminal or forge a log line.
1545
+ const folder = path.relative(workspaceDir, absoluteDir).replace(/\\/g, '/');
1546
+ const reason = err instanceof Error ? err.message : String(err);
1547
+ logger('workspace').error(`could not list every file under ${printable(folder)} for a delete: ${printable(reason)}`);
1548
+ throw Object.assign(new Error(`Could not list every file under "${folder}" — nothing was deleted. Try again, or ask an admin.`), {
1549
+ status: 500,
1550
+ });
1112
1551
  }
1113
- await walk(absoluteDir);
1114
1552
  return out;
1115
1553
  }
1116
1554
 
1117
1555
  /**
1118
- * Recursively remove empty directories under `absoluteDir`, bottom-up, then
1119
- * remove `absoluteDir` itself if it ends up empty. A directory is removed only
1120
- * if it contains nothing at the moment it's visited, so any file a concurrent
1121
- * writer dropped in mid-delete — and every parent directory on its path —
1122
- * survives. `.git` is left alone. Used after a recursive folder delete to
1123
- * sweep the leftover empty-folder shells off disk so the file tree (which
1124
- * lists on-disk directories, not just tracked files) stops showing the
1125
- * deleted container.
1556
+ * The error a failed folder keep answers: the original one — its type,
1557
+ * status and payload decide the response — with `message` saying what
1558
+ * landed and what did not. Anything that is not an Error becomes one.
1559
+ */
1560
+ function withKeptFolderContext(err: unknown, message: string): Error {
1561
+ if (!(err instanceof Error)) return new Error(message);
1562
+ err.message = message;
1563
+ return err;
1564
+ }
1565
+
1566
+ /**
1567
+ * Whether a folder exists and holds nothing. A folder that is gone answers
1568
+ * false: it was deleted explicitly, and keeping it would bring it back.
1126
1569
  */
1127
- async function removeEmptyDirs(absoluteDir: string): Promise<void> {
1128
- let entries: import('node:fs').Dirent[];
1570
+ async function isEmptyFolder(absoluteDir: string): Promise<boolean> {
1129
1571
  try {
1130
- entries = await fs.readdir(absoluteDir, { withFileTypes: true });
1131
- } catch {
1132
- // Already gone (raced delete) — nothing to do.
1133
- return;
1134
- }
1135
- for (const entry of entries) {
1136
- if (entry.name === '.git' && entry.isDirectory()) continue;
1137
- if (entry.isDirectory()) {
1138
- await removeEmptyDirs(path.join(absoluteDir, entry.name));
1139
- }
1140
- }
1141
- // Re-read after pruning children: a subdir we just emptied now lets this
1142
- // dir become removable too. Any surviving file (or `.git`) keeps it.
1143
- const remaining = await fs.readdir(absoluteDir);
1144
- if (remaining.length === 0) {
1145
- await fs.rmdir(absoluteDir);
1572
+ return (await fs.readdir(absoluteDir)).length === 0;
1573
+ } catch (err) {
1574
+ if (isAbsence(err)) return false;
1575
+ throw err;
1146
1576
  }
1147
1577
  }