@bevel-software/platform-core-backend 0.14.0 → 0.19.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 (1307) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -8
  2. package/dist/core/catalog-cache-invalidation.d.ts +45 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +76 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  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 +279 -64
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +66 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +290 -111
  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 +231 -15
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +83 -12
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +815 -212
  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 +312 -84
  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 +100 -25
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +162 -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/group-files.d.ts.map +1 -1
  120. package/dist/modules/access-model/group-files.js +4 -1
  121. package/dist/modules/access-model/group-files.js.map +1 -1
  122. package/dist/modules/access-model/plugin-principals.d.ts +51 -0
  123. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  124. package/dist/modules/access-model/plugin-principals.js +119 -0
  125. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  126. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  127. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  128. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  129. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  130. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  131. package/dist/modules/admin/admin-access.service.js +5 -2
  132. package/dist/modules/admin/admin-access.service.js.map +1 -1
  133. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  134. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  135. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  136. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  137. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  138. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  139. package/dist/modules/agent-instructions/compose.js +234 -0
  140. package/dist/modules/agent-instructions/compose.js.map +1 -0
  141. package/dist/modules/agent-instructions/index.d.ts +4 -0
  142. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  143. package/dist/modules/agent-instructions/index.js +4 -0
  144. package/dist/modules/agent-instructions/index.js.map +1 -0
  145. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  146. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  147. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  148. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  149. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  150. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  151. package/dist/modules/auth/account-erasure.service.js +96 -10
  152. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  153. package/dist/modules/auth/account.routes.d.ts +6 -3
  154. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  155. package/dist/modules/auth/account.routes.js +109 -10
  156. package/dist/modules/auth/account.routes.js.map +1 -1
  157. package/dist/modules/auth/auth.routes.d.ts +10 -3
  158. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  159. package/dist/modules/auth/auth.routes.js +9 -4
  160. package/dist/modules/auth/auth.routes.js.map +1 -1
  161. package/dist/modules/auth/auth.service.d.ts +89 -5
  162. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  163. package/dist/modules/auth/auth.service.js +160 -25
  164. package/dist/modules/auth/auth.service.js.map +1 -1
  165. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  166. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  167. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  168. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  169. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  170. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  171. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  172. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  173. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  174. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  175. package/dist/modules/database/advisory-lock.d.ts +103 -0
  176. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  177. package/dist/modules/database/advisory-lock.js +320 -0
  178. package/dist/modules/database/advisory-lock.js.map +1 -0
  179. package/dist/modules/database/connection.d.ts.map +1 -1
  180. package/dist/modules/database/connection.js +9 -1
  181. package/dist/modules/database/connection.js.map +1 -1
  182. package/dist/modules/database/core-schema.d.ts +672 -0
  183. package/dist/modules/database/core-schema.d.ts.map +1 -1
  184. package/dist/modules/database/core-schema.js +171 -0
  185. package/dist/modules/database/core-schema.js.map +1 -1
  186. package/dist/modules/database/migrate.d.ts.map +1 -1
  187. package/dist/modules/database/migrate.js +26 -6
  188. package/dist/modules/database/migrate.js.map +1 -1
  189. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  190. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  191. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  192. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  193. package/dist/modules/diff/diff.routes.js +2 -4
  194. package/dist/modules/diff/diff.routes.js.map +1 -1
  195. package/dist/modules/diff/diff.service.d.ts +4 -1
  196. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  197. package/dist/modules/diff/diff.service.js +79 -81
  198. package/dist/modules/diff/diff.service.js.map +1 -1
  199. package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
  200. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  201. package/dist/modules/kb-fs/bevel-ignore.js +71 -0
  202. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  203. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  204. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  205. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  206. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
  207. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  208. package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
  209. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  210. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  211. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  212. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  213. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  214. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  215. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  216. package/dist/modules/kb-fs/node-fs.js +158 -0
  217. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  218. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  219. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  220. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  221. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  222. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  223. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  224. package/dist/modules/kb-fs/repo-path.js +195 -3
  225. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  226. package/dist/modules/kb-sync/index.d.ts +6 -0
  227. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  228. package/dist/modules/kb-sync/index.js +5 -0
  229. package/dist/modules/kb-sync/index.js.map +1 -0
  230. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  231. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  232. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  233. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  234. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  235. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  236. package/dist/modules/kb-sync/kb-sync.routes.js +137 -0
  237. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  238. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  239. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  240. package/dist/modules/kb-sync/kb-sync.service.js +185 -0
  241. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  242. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  243. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  244. package/dist/modules/kb-sync/sync-auth.js +62 -0
  245. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  246. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  247. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  248. package/dist/modules/kb-sync/sync-payload.js +105 -0
  249. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  250. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  251. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  252. package/dist/modules/marketplace/git-http.routes.js +192 -0
  253. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  254. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +33 -0
  255. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  256. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +88 -0
  257. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  258. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  259. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  260. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  261. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  262. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +126 -0
  263. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  264. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +192 -0
  265. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  266. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  267. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  268. package/dist/modules/marketplace/github-facade/github-facade.routes.js +252 -0
  269. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  270. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +121 -0
  271. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  272. package/dist/modules/marketplace/github-facade/github-facade.service.js +213 -0
  273. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  274. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  275. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  276. package/dist/modules/marketplace/github-facade/index.js +6 -0
  277. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  278. package/dist/modules/marketplace/index.d.ts +4 -0
  279. package/dist/modules/marketplace/index.d.ts.map +1 -0
  280. package/dist/modules/marketplace/index.js +4 -0
  281. package/dist/modules/marketplace/index.js.map +1 -0
  282. package/dist/modules/marketplace/marketplace-repo.service.d.ts +135 -0
  283. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  284. package/dist/modules/marketplace/marketplace-repo.service.js +318 -0
  285. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  286. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  287. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  288. package/dist/modules/mcp/downstream-pool.js +221 -0
  289. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  290. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  291. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  292. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  293. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  294. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  295. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  296. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  297. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  298. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  299. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  300. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  301. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  302. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  303. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  304. package/dist/modules/mcp/first-call-probe.js +298 -0
  305. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  306. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  307. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  308. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  309. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  310. package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
  311. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  312. package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
  313. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  314. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  315. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  316. package/dist/modules/mcp/mcp.routes.js +83 -179
  317. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  318. package/dist/modules/mcp/mcp.service.d.ts +218 -61
  319. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  320. package/dist/modules/mcp/mcp.service.js +713 -159
  321. package/dist/modules/mcp/mcp.service.js.map +1 -1
  322. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  323. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +8 -2
  324. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  325. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  326. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  327. package/dist/modules/mcp/oauth/oauth-consent.routes.js +16 -2
  328. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  329. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  330. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  331. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  332. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  333. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  334. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  335. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  336. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  337. package/dist/modules/plugins/compile/compile-marketplace.d.ts +104 -0
  338. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  339. package/dist/modules/plugins/compile/compile-marketplace.js +323 -0
  340. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  341. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +73 -0
  342. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  343. package/dist/modules/plugins/compile/marketplace-compiler.service.js +135 -0
  344. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  345. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  346. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  347. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +158 -0
  348. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  349. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +65 -0
  350. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  351. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +204 -0
  352. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  353. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +39 -0
  354. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  355. package/dist/modules/plugins/discovery/kb-plugin-source.js +110 -0
  356. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  357. package/dist/modules/plugins/discovery/native.source.d.ts +16 -0
  358. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  359. package/dist/modules/plugins/discovery/native.source.js +120 -0
  360. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  361. package/dist/modules/plugins/discovery/plugin-source.d.ts +110 -0
  362. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  363. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  364. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  365. package/dist/modules/plugins/index.d.ts +15 -3
  366. package/dist/modules/plugins/index.d.ts.map +1 -1
  367. package/dist/modules/plugins/index.js +13 -2
  368. package/dist/modules/plugins/index.js.map +1 -1
  369. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  370. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  371. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  372. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  373. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  374. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  375. package/dist/modules/plugins/join-request-records.store.js +179 -0
  376. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  377. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  378. package/dist/modules/plugins/join-requests.service.js +4 -2
  379. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  380. package/dist/modules/plugins/plugin-links.d.ts +69 -0
  381. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  382. package/dist/modules/plugins/plugin-links.js +163 -0
  383. package/dist/modules/plugins/plugin-links.js.map +1 -0
  384. package/dist/modules/plugins/plugin-links.service.d.ts +111 -0
  385. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  386. package/dist/modules/plugins/plugin-links.service.js +295 -0
  387. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  388. package/dist/modules/plugins/plugin-provision.service.d.ts +129 -21
  389. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  390. package/dist/modules/plugins/plugin-provision.service.js +320 -75
  391. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  392. package/dist/modules/plugins/plugin-rename.service.d.ts +71 -0
  393. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  394. package/dist/modules/plugins/plugin-rename.service.js +254 -0
  395. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  396. package/dist/modules/plugins/plugins.contract.d.ts +78 -4
  397. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  398. package/dist/modules/plugins/plugins.routes.d.ts +44 -5
  399. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  400. package/dist/modules/plugins/plugins.routes.js +297 -115
  401. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  402. package/dist/modules/plugins/plugins.service.d.ts +82 -3
  403. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  404. package/dist/modules/plugins/plugins.service.js +178 -48
  405. package/dist/modules/plugins/plugins.service.js.map +1 -1
  406. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  407. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  408. package/dist/modules/plugins/plugins.tools.js +82 -0
  409. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  410. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  411. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  412. package/dist/modules/plugins/teams.routes.js +79 -0
  413. package/dist/modules/plugins/teams.routes.js.map +1 -0
  414. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  415. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  416. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  417. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  418. package/dist/modules/secrets-vault/index.d.ts +1 -1
  419. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  420. package/dist/modules/secrets-vault/index.js.map +1 -1
  421. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  422. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  423. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  424. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  425. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  426. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  427. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  428. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  429. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  430. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  431. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  432. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  433. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  434. package/dist/modules/settings/connection-check.d.ts +118 -0
  435. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  436. package/dist/modules/settings/connection-check.js +287 -0
  437. package/dist/modules/settings/connection-check.js.map +1 -0
  438. package/dist/modules/settings/deployment-settings.service.d.ts +157 -2
  439. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  440. package/dist/modules/settings/deployment-settings.service.js +412 -35
  441. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  442. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  443. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  444. package/dist/modules/settings/git-root-folders.js +130 -0
  445. package/dist/modules/settings/git-root-folders.js.map +1 -0
  446. package/dist/modules/settings/oidc-check.d.ts +85 -0
  447. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  448. package/dist/modules/settings/oidc-check.js +172 -0
  449. package/dist/modules/settings/oidc-check.js.map +1 -0
  450. package/dist/modules/settings/setup.routes.d.ts +57 -1
  451. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  452. package/dist/modules/settings/setup.routes.js +408 -108
  453. package/dist/modules/settings/setup.routes.js.map +1 -1
  454. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  455. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  456. package/dist/modules/skills/allowed-tools-check.js +260 -0
  457. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  458. package/dist/modules/skills/index.d.ts +4 -1
  459. package/dist/modules/skills/index.d.ts.map +1 -1
  460. package/dist/modules/skills/index.js +2 -0
  461. package/dist/modules/skills/index.js.map +1 -1
  462. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  463. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  464. package/dist/modules/skills/pending-skills.service.js +24 -71
  465. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  466. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  467. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  468. package/dist/modules/skills/skill-access-requests.routes.js +171 -0
  469. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  470. package/dist/modules/skills/skills.contract.d.ts +67 -2
  471. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  472. package/dist/modules/skills/skills.routes.d.ts +35 -3
  473. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  474. package/dist/modules/skills/skills.routes.js +57 -4
  475. package/dist/modules/skills/skills.routes.js.map +1 -1
  476. package/dist/modules/skills/skills.service.d.ts +31 -3
  477. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  478. package/dist/modules/skills/skills.service.js +236 -65
  479. package/dist/modules/skills/skills.service.js.map +1 -1
  480. package/dist/modules/skills/skills.tools.d.ts +4 -1
  481. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  482. package/dist/modules/skills/skills.tools.js +41 -6
  483. package/dist/modules/skills/skills.tools.js.map +1 -1
  484. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  485. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  486. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  487. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  488. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  489. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  490. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  491. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  492. package/dist/modules/tool-auth/external-api-key.interface.d.ts +66 -1
  493. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  494. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  495. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  496. package/dist/modules/tool-auth/external-api-key.service.d.ts +23 -4
  497. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  498. package/dist/modules/tool-auth/external-api-key.service.js +79 -19
  499. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  500. package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
  501. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  502. package/dist/modules/tool-auth/internal-token.service.js +5 -2
  503. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  504. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  505. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  506. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  507. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  508. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
  509. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  510. package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
  511. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  512. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  513. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  514. package/dist/modules/tool-helpers/tool-context.js +28 -6
  515. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  516. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  517. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  518. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  519. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  520. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  521. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  522. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  523. package/dist/modules/tool-manuals/index.d.ts +2 -1
  524. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  525. package/dist/modules/tool-manuals/index.js +1 -0
  526. package/dist/modules/tool-manuals/index.js.map +1 -1
  527. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  528. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  529. package/dist/modules/tool-manuals/mcp-json-discovery.js +136 -96
  530. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  531. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  532. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  533. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  534. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  535. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  536. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  537. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  538. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  539. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  540. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  541. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  542. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  543. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  544. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  545. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  546. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  547. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  548. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  549. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  550. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  551. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +121 -2
  552. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  553. package/dist/modules/tool-manuals/tool-manuals.service.js +415 -92
  554. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  555. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  556. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  557. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  558. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  559. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  560. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  561. package/dist/modules/workflow/event-bus.d.ts +10 -4
  562. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  563. package/dist/modules/workflow/event-bus.js +14 -11
  564. package/dist/modules/workflow/event-bus.js.map +1 -1
  565. package/dist/modules/workflow/events.routes.d.ts +17 -2
  566. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  567. package/dist/modules/workflow/events.routes.js +31 -12
  568. package/dist/modules/workflow/events.routes.js.map +1 -1
  569. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  570. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  571. package/dist/modules/workflow/file-lock.service.js +27 -4
  572. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  573. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  574. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  575. package/dist/modules/workflow/git/change-request-link.js +34 -0
  576. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  577. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  578. package/dist/modules/workflow/git/git-version.js +3 -1
  579. package/dist/modules/workflow/git/git-version.js.map +1 -1
  580. package/dist/modules/workflow/git/git.service.d.ts +303 -6
  581. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  582. package/dist/modules/workflow/git/git.service.js +753 -170
  583. package/dist/modules/workflow/git/git.service.js.map +1 -1
  584. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  585. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  586. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  587. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  588. package/dist/modules/workflow/git/pull-request.service.d.ts +57 -2
  589. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  590. package/dist/modules/workflow/git/pull-request.service.js +165 -25
  591. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  592. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  593. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  594. package/dist/modules/workflow/pending-commits.service.js +24 -4
  595. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  596. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  597. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  598. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  599. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  600. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  601. package/dist/modules/workflow/recovery-bot.js +3 -1
  602. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  603. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  604. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  605. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  606. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  607. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  608. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  609. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  610. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  611. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  612. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  613. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  614. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  615. package/dist/modules/workflow/sanitize-error.js +8 -4
  616. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  617. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  618. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  619. package/dist/modules/workflow/workflow.routes.js +259 -29
  620. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  621. package/dist/modules/workflow/workflow.service.d.ts +217 -14
  622. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  623. package/dist/modules/workflow/workflow.service.js +1142 -109
  624. package/dist/modules/workflow/workflow.service.js.map +1 -1
  625. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  626. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  627. package/dist/modules/workspace/empty-dirs.js +90 -0
  628. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  629. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  630. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  631. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  632. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  633. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  634. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  635. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  636. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  637. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  638. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  639. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  640. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  641. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  642. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  643. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  644. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  645. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  646. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  647. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  648. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  649. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  650. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  651. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  652. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  653. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  654. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  655. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  656. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  657. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  658. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  659. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  660. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  661. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  662. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  663. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  664. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  665. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  666. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  667. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  668. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  669. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  670. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  671. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  672. package/dist/modules/workspace/not-found.d.ts +80 -0
  673. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  674. package/dist/modules/workspace/not-found.js +112 -0
  675. package/dist/modules/workspace/not-found.js.map +1 -0
  676. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  677. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  678. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  679. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  680. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  681. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  682. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  683. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  684. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  685. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  686. package/dist/modules/workspace/startup/kb-git.js +34 -35
  687. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  688. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  689. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  690. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  691. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  692. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  693. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  694. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  695. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  696. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  697. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  698. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  699. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  700. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  701. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  702. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  703. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  704. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  705. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  706. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  707. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  708. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +41 -0
  709. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  710. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +85 -0
  711. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  712. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  713. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  714. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  715. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  716. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  717. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  718. package/dist/modules/workspace/startup/steps/seed-tree.js +174 -54
  719. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  720. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  721. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  722. package/dist/modules/workspace/startup/steps/template-files.step.js +521 -126
  723. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  724. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  725. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  726. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  727. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  728. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  729. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  730. package/dist/modules/workspace/workspace.routes.js +657 -246
  731. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  732. package/dist/modules/workspace/workspace.service.d.ts +323 -12
  733. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  734. package/dist/modules/workspace/workspace.service.js +1006 -257
  735. package/dist/modules/workspace/workspace.service.js.map +1 -1
  736. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  737. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  738. package/dist/modules/workspace/workspace.tools.js +1590 -146
  739. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  740. package/dist/modules/workspace/write-denial.d.ts +76 -0
  741. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  742. package/dist/modules/workspace/write-denial.js +138 -0
  743. package/dist/modules/workspace/write-denial.js.map +1 -0
  744. package/dist/shared/canonical-file-identity.d.ts +64 -0
  745. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  746. package/dist/shared/canonical-file-identity.js +92 -0
  747. package/dist/shared/canonical-file-identity.js.map +1 -0
  748. package/dist/shared/domain-errors.d.ts +128 -0
  749. package/dist/shared/domain-errors.d.ts.map +1 -1
  750. package/dist/shared/domain-errors.js +166 -0
  751. package/dist/shared/domain-errors.js.map +1 -1
  752. package/dist/shared/email-identity.d.ts +32 -0
  753. package/dist/shared/email-identity.d.ts.map +1 -0
  754. package/dist/shared/email-identity.js +37 -0
  755. package/dist/shared/email-identity.js.map +1 -0
  756. package/dist/shared/fs-errors.d.ts +8 -0
  757. package/dist/shared/fs-errors.d.ts.map +1 -0
  758. package/dist/shared/fs-errors.js +11 -0
  759. package/dist/shared/fs-errors.js.map +1 -0
  760. package/dist/shared/fs-walk.d.ts +20 -6
  761. package/dist/shared/fs-walk.d.ts.map +1 -1
  762. package/dist/shared/fs-walk.js +35 -27
  763. package/dist/shared/fs-walk.js.map +1 -1
  764. package/dist/shared/fs.contract.d.ts +206 -0
  765. package/dist/shared/fs.contract.d.ts.map +1 -0
  766. package/dist/shared/fs.contract.js +79 -0
  767. package/dist/shared/fs.contract.js.map +1 -0
  768. package/dist/shared/git-failure.d.ts +80 -0
  769. package/dist/shared/git-failure.d.ts.map +1 -0
  770. package/dist/shared/git-failure.js +139 -0
  771. package/dist/shared/git-failure.js.map +1 -0
  772. package/dist/shared/git-internals.d.ts +17 -0
  773. package/dist/shared/git-internals.d.ts.map +1 -0
  774. package/dist/shared/git-internals.js +123 -0
  775. package/dist/shared/git-internals.js.map +1 -0
  776. package/dist/shared/git.contract.d.ts +157 -0
  777. package/dist/shared/git.contract.d.ts.map +1 -0
  778. package/dist/shared/git.contract.js +103 -0
  779. package/dist/shared/git.contract.js.map +1 -0
  780. package/dist/shared/http-errors.d.ts +33 -0
  781. package/dist/shared/http-errors.d.ts.map +1 -0
  782. package/dist/shared/http-errors.js +35 -0
  783. package/dist/shared/http-errors.js.map +1 -0
  784. package/dist/shared/kb-walk.d.ts +52 -0
  785. package/dist/shared/kb-walk.d.ts.map +1 -0
  786. package/dist/shared/kb-walk.js +76 -0
  787. package/dist/shared/kb-walk.js.map +1 -0
  788. package/dist/shared/logger.contract.d.ts +63 -0
  789. package/dist/shared/logger.contract.d.ts.map +1 -0
  790. package/dist/shared/logger.contract.js +126 -0
  791. package/dist/shared/logger.contract.js.map +1 -0
  792. package/dist/shared/logging.d.ts +31 -0
  793. package/dist/shared/logging.d.ts.map +1 -0
  794. package/dist/shared/logging.js +92 -0
  795. package/dist/shared/logging.js.map +1 -0
  796. package/dist/shared/path-containment.d.ts +21 -0
  797. package/dist/shared/path-containment.d.ts.map +1 -0
  798. package/dist/shared/path-containment.js +34 -0
  799. package/dist/shared/path-containment.js.map +1 -0
  800. package/dist/shared/path-order.d.ts +18 -0
  801. package/dist/shared/path-order.d.ts.map +1 -0
  802. package/dist/shared/path-order.js +35 -0
  803. package/dist/shared/path-order.js.map +1 -0
  804. package/dist/shared/pending-proposals.d.ts +71 -0
  805. package/dist/shared/pending-proposals.d.ts.map +1 -0
  806. package/dist/shared/pending-proposals.js +98 -0
  807. package/dist/shared/pending-proposals.js.map +1 -0
  808. package/dist/shared/printable.d.ts +22 -0
  809. package/dist/shared/printable.d.ts.map +1 -0
  810. package/dist/shared/printable.js +65 -0
  811. package/dist/shared/printable.js.map +1 -0
  812. package/dist/shared/redact-secret.d.ts +30 -0
  813. package/dist/shared/redact-secret.d.ts.map +1 -0
  814. package/dist/shared/redact-secret.js +114 -0
  815. package/dist/shared/redact-secret.js.map +1 -0
  816. package/dist/shared/rename-no-replace.d.ts +119 -0
  817. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  818. package/dist/shared/rename-no-replace.js +323 -0
  819. package/dist/shared/rename-no-replace.js.map +1 -0
  820. package/dist/shared/ssrf.d.ts.map +1 -1
  821. package/dist/shared/ssrf.js +49 -18
  822. package/dist/shared/ssrf.js.map +1 -1
  823. package/dist/shared/ttl-cache.d.ts +28 -1
  824. package/dist/shared/ttl-cache.d.ts.map +1 -1
  825. package/dist/shared/ttl-cache.js +33 -1
  826. package/dist/shared/ttl-cache.js.map +1 -1
  827. package/dist/version.d.ts +10 -6
  828. package/dist/version.d.ts.map +1 -1
  829. package/dist/version.js +10 -6
  830. package/dist/version.js.map +1 -1
  831. package/kb-template/.bevelignore +4 -3
  832. package/kb-template/AGENTS.md +364 -54
  833. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  834. package/kb-template/Skills/.gitkeep +0 -0
  835. package/kb-template/access.md +5 -3
  836. package/kb-template/mcp-description.md +27 -0
  837. package/migrations/0005_github_facade.sql +25 -0
  838. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  839. package/migrations/0007_github_facade_registered.sql +1 -0
  840. package/migrations/0008_change_request_apply_failure.sql +4 -0
  841. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  842. package/migrations/0010_plugin_join_requests.sql +15 -0
  843. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  844. package/migrations/meta/0005_snapshot.json +1717 -0
  845. package/migrations/meta/0006_snapshot.json +1723 -0
  846. package/migrations/meta/0007_snapshot.json +1729 -0
  847. package/migrations/meta/0008_snapshot.json +1753 -0
  848. package/migrations/meta/0009_snapshot.json +1759 -0
  849. package/migrations/meta/0010_snapshot.json +1872 -0
  850. package/migrations/meta/0011_snapshot.json +1884 -0
  851. package/migrations/meta/_journal.json +49 -0
  852. package/package.json +8 -7
  853. package/src/__tests__/core-config.domain.test.ts +67 -2
  854. package/src/__tests__/kb-layout-config.test.ts +295 -0
  855. package/src/__tests__/open-change-gate.ts +15 -0
  856. package/src/core/__tests__/catalog-cache-invalidation.test.ts +226 -0
  857. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  858. package/src/core/__tests__/lifecycle.test.ts +369 -0
  859. package/src/core/__tests__/public-config.test.ts +30 -0
  860. package/src/core/__tests__/readiness.test.ts +109 -0
  861. package/src/core/catalog-cache-invalidation.ts +111 -0
  862. package/src/core/catalog-revision.ts +110 -0
  863. package/src/core/create-core-server.ts +358 -63
  864. package/src/core/create-core-services.ts +449 -107
  865. package/src/core/lifecycle.ts +312 -0
  866. package/src/core/public-config.ts +86 -0
  867. package/src/core/readiness.ts +123 -0
  868. package/src/core-config.ts +70 -4
  869. package/src/index.ts +15 -0
  870. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  871. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  872. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  873. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  874. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  875. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  876. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  877. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +88 -0
  878. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  879. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  880. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  881. package/src/modules/access/__tests__/access-control.service.test.ts +142 -81
  882. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  883. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  884. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  885. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  886. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  887. package/src/modules/access/__tests__/access-plugin-principals.test.ts +385 -0
  888. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  889. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  890. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  891. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  892. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  893. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  894. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  895. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  896. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  897. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  898. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  899. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  900. package/src/modules/access/__tests__/groups-admin.service.test.ts +14 -5
  901. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  902. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  903. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  904. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  905. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  906. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  907. package/src/modules/access/access-control.interface.ts +269 -18
  908. package/src/modules/access/access-control.service.ts +943 -226
  909. package/src/modules/access/access-mutation.service.ts +182 -13
  910. package/src/modules/access/access-view.ts +316 -0
  911. package/src/modules/access/access.routes.ts +385 -105
  912. package/src/modules/access/admin-locked-commit.ts +12 -14
  913. package/src/modules/access/admin-route-helpers.ts +4 -4
  914. package/src/modules/access/change-read-gate.ts +148 -0
  915. package/src/modules/access/creator-access.ts +89 -124
  916. package/src/modules/access/directory-sync-bot.ts +4 -1
  917. package/src/modules/access/groups-admin.service.ts +3 -3
  918. package/src/modules/access/reference-scan.ts +5 -5
  919. package/src/modules/access/roles-admin.service.ts +94 -15
  920. package/src/modules/access/synced-groups-committer.ts +10 -15
  921. package/src/modules/access/synced-groups-writer.ts +3 -12
  922. package/src/modules/access/user-access-removal.service.ts +711 -0
  923. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  924. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  925. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  926. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  927. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  928. package/src/modules/access-model/access-errors.ts +56 -15
  929. package/src/modules/access-model/access-grammar.ts +198 -49
  930. package/src/modules/access-model/access-splice.ts +18 -18
  931. package/src/modules/access-model/access-template.ts +44 -0
  932. package/src/modules/access-model/change-gate.ts +80 -0
  933. package/src/modules/access-model/creator.ts +18 -21
  934. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  935. package/src/modules/access-model/group-files.ts +4 -0
  936. package/src/modules/access-model/plugin-principals.ts +130 -0
  937. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  938. package/src/modules/admin/admin-access.service.ts +6 -2
  939. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  940. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  941. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  942. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  943. package/src/modules/agent-instructions/compose.ts +271 -0
  944. package/src/modules/agent-instructions/index.ts +14 -0
  945. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  946. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  947. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  948. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  949. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  950. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  951. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  952. package/src/modules/auth/account-erasure.service.ts +117 -10
  953. package/src/modules/auth/account.routes.ts +115 -14
  954. package/src/modules/auth/auth.routes.ts +20 -7
  955. package/src/modules/auth/auth.service.ts +187 -28
  956. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  957. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  958. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  959. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  960. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  961. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  962. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  963. package/src/modules/database/advisory-lock.ts +346 -0
  964. package/src/modules/database/connection.ts +9 -1
  965. package/src/modules/database/core-schema.ts +177 -0
  966. package/src/modules/database/migrate.ts +27 -6
  967. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  968. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  969. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  970. package/src/modules/diff/diff.routes.ts +2 -4
  971. package/src/modules/diff/diff.service.ts +75 -79
  972. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  973. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  974. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  975. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  976. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  977. package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
  978. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  979. package/src/modules/kb-fs/node-fs.ts +165 -0
  980. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  981. package/src/modules/kb-fs/repo-path.ts +213 -3
  982. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  983. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  984. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  985. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  986. package/src/modules/kb-sync/index.ts +19 -0
  987. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  988. package/src/modules/kb-sync/kb-sync.routes.ts +200 -0
  989. package/src/modules/kb-sync/kb-sync.service.ts +204 -0
  990. package/src/modules/kb-sync/sync-auth.ts +118 -0
  991. package/src/modules/kb-sync/sync-payload.ts +143 -0
  992. package/src/modules/marketplace/__tests__/github-facade.test.ts +575 -0
  993. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +180 -0
  994. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  995. package/src/modules/marketplace/git-http.routes.ts +196 -0
  996. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +110 -0
  997. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  998. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +263 -0
  999. package/src/modules/marketplace/github-facade/github-facade.routes.ts +294 -0
  1000. package/src/modules/marketplace/github-facade/github-facade.service.ts +282 -0
  1001. package/src/modules/marketplace/github-facade/index.ts +26 -0
  1002. package/src/modules/marketplace/index.ts +3 -0
  1003. package/src/modules/marketplace/marketplace-repo.service.ts +352 -0
  1004. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  1005. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  1006. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  1007. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  1008. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  1009. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  1010. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
  1011. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  1012. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  1013. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  1014. package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
  1015. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  1016. package/src/modules/mcp/downstream-pool.ts +256 -0
  1017. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  1018. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  1019. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  1020. package/src/modules/mcp/first-call-probe.ts +418 -0
  1021. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  1022. package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
  1023. package/src/modules/mcp/mcp.routes.ts +376 -482
  1024. package/src/modules/mcp/mcp.service.ts +846 -176
  1025. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +8 -2
  1026. package/src/modules/mcp/oauth/oauth-consent.routes.ts +28 -6
  1027. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  1028. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  1029. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +166 -0
  1030. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  1031. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +423 -0
  1032. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  1033. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  1034. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  1035. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +201 -16
  1036. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +284 -0
  1037. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  1038. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +330 -20
  1039. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +456 -0
  1040. package/src/modules/plugins/__tests__/plugins.routes.test.ts +441 -43
  1041. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  1042. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  1043. package/src/modules/plugins/compile/compile-marketplace.ts +500 -0
  1044. package/src/modules/plugins/compile/marketplace-compiler.service.ts +151 -0
  1045. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +365 -0
  1046. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +161 -0
  1047. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +250 -0
  1048. package/src/modules/plugins/discovery/kb-plugin-source.ts +117 -0
  1049. package/src/modules/plugins/discovery/native.source.ts +140 -0
  1050. package/src/modules/plugins/discovery/plugin-source.ts +113 -0
  1051. package/src/modules/plugins/index.ts +25 -2
  1052. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  1053. package/src/modules/plugins/join-request-records.store.ts +353 -0
  1054. package/src/modules/plugins/join-requests.service.ts +7 -4
  1055. package/src/modules/plugins/plugin-links.service.ts +350 -0
  1056. package/src/modules/plugins/plugin-links.ts +187 -0
  1057. package/src/modules/plugins/plugin-provision.service.ts +356 -78
  1058. package/src/modules/plugins/plugin-rename.service.ts +310 -0
  1059. package/src/modules/plugins/plugins.contract.ts +79 -4
  1060. package/src/modules/plugins/plugins.routes.ts +320 -123
  1061. package/src/modules/plugins/plugins.service.ts +184 -57
  1062. package/src/modules/plugins/plugins.tools.ts +88 -0
  1063. package/src/modules/plugins/teams.routes.ts +129 -0
  1064. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  1065. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  1066. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  1067. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  1068. package/src/modules/secrets-vault/index.ts +1 -0
  1069. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  1070. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  1071. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  1072. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  1073. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  1074. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +240 -1
  1075. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  1076. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  1077. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  1078. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1079. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1080. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  1081. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1082. package/src/modules/settings/connection-check.ts +351 -0
  1083. package/src/modules/settings/deployment-settings.service.ts +478 -39
  1084. package/src/modules/settings/git-root-folders.ts +171 -0
  1085. package/src/modules/settings/oidc-check.ts +229 -0
  1086. package/src/modules/settings/setup.routes.ts +480 -110
  1087. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1088. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1089. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1090. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  1091. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1092. package/src/modules/skills/__tests__/skills.service.test.ts +308 -2
  1093. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1094. package/src/modules/skills/index.ts +5 -0
  1095. package/src/modules/skills/pending-skills.service.ts +26 -85
  1096. package/src/modules/skills/skill-access-requests.routes.ts +203 -0
  1097. package/src/modules/skills/skills.contract.ts +67 -3
  1098. package/src/modules/skills/skills.routes.ts +75 -4
  1099. package/src/modules/skills/skills.service.ts +258 -66
  1100. package/src/modules/skills/skills.tools.ts +43 -5
  1101. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1102. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1103. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
  1104. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1105. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1106. package/src/modules/tool-auth/external-api-key.interface.ts +73 -1
  1107. package/src/modules/tool-auth/external-api-key.service.ts +95 -29
  1108. package/src/modules/tool-auth/internal-token.service.ts +5 -2
  1109. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1110. package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
  1111. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1112. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1113. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1114. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1115. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1116. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  1117. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1118. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1119. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1120. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1121. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1122. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +11 -4
  1123. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1124. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1125. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1126. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1127. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1128. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1129. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1130. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1131. package/src/modules/tool-manuals/index.ts +3 -0
  1132. package/src/modules/tool-manuals/mcp-json-discovery.ts +164 -95
  1133. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1134. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1135. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1136. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1137. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1138. package/src/modules/tool-manuals/tool-manuals.service.ts +440 -104
  1139. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1140. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1141. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1142. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1143. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1144. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1145. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1146. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1147. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +3 -1
  1148. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1149. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1150. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1151. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1152. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1153. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +3 -0
  1154. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1155. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +296 -27
  1156. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1157. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1158. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1159. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +133 -2
  1160. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +442 -0
  1161. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1162. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1163. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1164. package/src/modules/workflow/event-bus.ts +26 -28
  1165. package/src/modules/workflow/events.routes.ts +56 -21
  1166. package/src/modules/workflow/file-lock.service.ts +27 -4
  1167. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1168. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1169. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1170. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1171. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1172. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1173. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  1174. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  1175. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1176. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  1177. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1178. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1179. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1180. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1181. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1182. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1183. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1184. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1185. package/src/modules/workflow/git/git-version.ts +5 -2
  1186. package/src/modules/workflow/git/git.service.ts +922 -199
  1187. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1188. package/src/modules/workflow/git/pull-request.service.ts +188 -27
  1189. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1190. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1191. package/src/modules/workflow/recovery-bot.ts +4 -1
  1192. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1193. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1194. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1195. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1196. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1197. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1198. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1199. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1200. package/src/modules/workflow/sanitize-error.ts +9 -4
  1201. package/src/modules/workflow/workflow.routes.ts +273 -29
  1202. package/src/modules/workflow/workflow.service.ts +1341 -174
  1203. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1204. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1205. package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
  1206. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1207. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1208. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1209. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1210. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1211. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1212. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1213. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1214. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1215. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +31 -13
  1216. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1217. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +94 -0
  1218. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  1219. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1220. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +98 -13
  1221. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1222. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1223. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1224. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1225. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1226. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1227. package/src/modules/workspace/empty-dirs.ts +79 -0
  1228. package/src/modules/workspace/file-readers/README.md +32 -0
  1229. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1230. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1231. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1232. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1233. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1234. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1235. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1236. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1237. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1238. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1239. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1240. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1241. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1242. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1243. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1244. package/src/modules/workspace/not-found.ts +119 -0
  1245. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1246. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1247. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1248. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1249. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1250. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1251. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1252. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1405 -48
  1253. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1254. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1255. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1256. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1257. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +90 -0
  1258. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1259. package/src/modules/workspace/startup/steps/seed-tree.ts +177 -54
  1260. package/src/modules/workspace/startup/steps/template-files.step.ts +581 -126
  1261. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1262. package/src/modules/workspace/workspace.routes.ts +691 -269
  1263. package/src/modules/workspace/workspace.service.ts +1071 -292
  1264. package/src/modules/workspace/workspace.tools.ts +1787 -180
  1265. package/src/modules/workspace/write-denial.ts +189 -0
  1266. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1267. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1268. package/src/shared/__tests__/filename.test.ts +0 -0
  1269. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1270. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1271. package/src/shared/__tests__/git-internals.test.ts +115 -0
  1272. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1273. package/src/shared/__tests__/logging.test.ts +185 -0
  1274. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1275. package/src/shared/__tests__/path-order.test.ts +24 -0
  1276. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1277. package/src/shared/__tests__/printable.test.ts +64 -0
  1278. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1279. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1280. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1281. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  1282. package/src/shared/canonical-file-identity.ts +92 -0
  1283. package/src/shared/domain-errors.ts +178 -0
  1284. package/src/shared/email-identity.ts +38 -0
  1285. package/src/shared/fs.contract.ts +225 -0
  1286. package/src/shared/git-failure.ts +196 -0
  1287. package/src/shared/git-internals.ts +124 -0
  1288. package/src/shared/git.contract.ts +170 -0
  1289. package/src/shared/http-errors.ts +36 -0
  1290. package/src/shared/logger.contract.ts +141 -0
  1291. package/src/shared/logging.ts +96 -0
  1292. package/src/shared/path-containment.ts +34 -0
  1293. package/src/shared/path-order.ts +33 -0
  1294. package/src/shared/pending-proposals.ts +161 -0
  1295. package/src/shared/printable.ts +68 -0
  1296. package/src/shared/redact-secret.ts +118 -0
  1297. package/src/shared/rename-no-replace.ts +386 -0
  1298. package/src/shared/ssrf.ts +44 -16
  1299. package/src/shared/ttl-cache.ts +33 -1
  1300. package/src/version.ts +10 -6
  1301. package/src/modules/diff/diff-paths.ts +0 -9
  1302. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1303. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1304. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1305. package/src/shared/__tests__/fs-walk.test.ts +0 -42
  1306. package/src/shared/fs-walk.ts +0 -30
  1307. package/src/shared/hash-email.ts +0 -14
@@ -1,13 +1,45 @@
1
1
  import fs from 'node:fs/promises';
2
+ import { logger } from '../../shared/logging.js';
3
+
4
+ const log = logger('workspace');
2
5
  import path from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
6
  import AdmZip from 'adm-zip';
6
7
  import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '@bevel-software/platform-shared';
7
- import { assertValidRelativePath, validateFilename, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
8
- import { BevelIgnoreStack } from './bevel-ignore.js';
8
+ import { AsyncLocalStorage } from 'node:async_hooks';
9
+ import {
10
+ validateRelativePath,
11
+ validateFilename,
12
+ DEFAULT_BRANCH,
13
+ FOLDER_PLACEHOLDER,
14
+ isFolderPlaceholder,
15
+ entryExistsMessage,
16
+ type ExistingEntryKind,
17
+ } from '@bevel-software/platform-shared';
18
+ import { isAbsence, type ITreeWalker, type TreeWalkOptions } from '../../shared/fs.contract.js';
19
+ import {
20
+ DestinationTakenError,
21
+ inspectDestination,
22
+ renameNoReplace,
23
+ } from '../../shared/rename-no-replace.js';
24
+ import type { IGitRunner } from '../../shared/git.contract.js';
25
+ import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
26
+ import { assertWithinDirectory } from '../../shared/path-containment.js';
27
+ import { assertRepoRootNameFree, normalizeWorkspacePath } from '../kb-fs/repo-path.js';
28
+ import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
29
+ import {
30
+ GitInternalsError,
31
+ PathNotFoundError,
32
+ PathTraversalError,
33
+ UnreadableArchiveError,
34
+ WorkflowValidationError,
35
+ } from '../../shared/domain-errors.js';
9
36
  import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
10
- import { WorkflowDomainError } from '../../shared/domain-errors.js';
37
+ import {
38
+ BranchNotFoundError,
39
+ RemoteBranchGoneError,
40
+ WorkflowDomainError,
41
+ isMissingRemoteBranchFailure,
42
+ } from '../../shared/domain-errors.js';
11
43
  import { assertValidBranchName } from '../kb-fs/branch-name.js';
12
44
  import {
13
45
  cloneCredentialArgs,
@@ -17,6 +49,11 @@ import {
17
49
  } from '../kb-fs/clone-config.js';
18
50
  import type { IDiffService } from '../diff/diff.interface.js';
19
51
 
52
+ /** One held path turn: the settled tail of the nested mutations launched inside it. */
53
+ interface HeldTurn {
54
+ tail: Promise<void>;
55
+ }
56
+
20
57
  /**
21
58
  * Workspaces are per-branch, not per-user (PLAN §3). One on-disk clone per
22
59
  * branch name, shared by every user editing that branch — coordination of
@@ -72,7 +109,36 @@ export class FolderTooLargeError extends Error {
72
109
  }
73
110
  }
74
111
 
75
- const execFileAsync = promisify(execFile);
112
+ /**
113
+ * A rename or move would land on a name something else already has. 409, like
114
+ * every other precondition the caller has to clear first: nothing was moved,
115
+ * and the entry already there is untouched.
116
+ *
117
+ * Lives here rather than in `shared/domain-errors.ts` because the sentence is
118
+ * built by `entryExistsMessage` and that file deliberately imports no VALUE
119
+ * from `@bevel-software/platform-shared`, only a type. It still extends
120
+ * `WorkflowDomainError`, so the routes' `sendError` maps it to 409 with
121
+ * `{ kind: 'entry-exists', … }` without knowing this class exists.
122
+ */
123
+ export class EntryExistsError extends WorkflowDomainError {
124
+ readonly kind = 'entry-exists' as const;
125
+ constructor(readonly entryKind: ExistingEntryKind, readonly destination: string) {
126
+ super(entryExistsMessage(entryKind, destination), 409, {
127
+ kind: 'entry-exists',
128
+ entryKind,
129
+ destination,
130
+ });
131
+ this.name = 'EntryExistsError';
132
+ }
133
+ }
134
+
135
+ /**
136
+ * A floor under the git deadline for a clone: a first clone of a large
137
+ * knowledge base over a slow link legitimately takes minutes, and the port's
138
+ * default is sized for a running deployment's operations. A configured
139
+ * ceiling above this applies as configured.
140
+ */
141
+ const CLONE_TIMEOUT_MS = 600_000;
76
142
 
77
143
  /**
78
144
  * Identity stamped on the per-branch clone's git config. Every workflow
@@ -92,14 +158,75 @@ function redactError(err: unknown): string {
92
158
 
93
159
  const FETCH_CACHE_TTL_MS = 30_000;
94
160
 
161
+ /**
162
+ * The bytes a conditional write compares against. An absent file reads as the
163
+ * empty string, so `expectedContent: ''` means "expect nothing there yet".
164
+ *
165
+ * A directory at the path is a client error, not a 500 with a raw OS message:
166
+ * `GET /file` counts EISDIR as "no file at this path", so the editor that
167
+ * opened empty on that 404 would otherwise be told its save failed for an
168
+ * unexplained internal reason. Anything else (EACCES, EIO) is a real read
169
+ * failure and propagates: an unreadable file must not pass for an empty one.
170
+ */
171
+ async function readForConditionalWrite(absolutePath: string, relativePath: string): Promise<string> {
172
+ try {
173
+ return await fs.readFile(absolutePath, 'utf-8');
174
+ } catch (err) {
175
+ if (isAbsence(err)) return '';
176
+ const code = (err as NodeJS.ErrnoException).code;
177
+ if (code === 'EISDIR') {
178
+ const notAFile: Error & { status?: number } = new Error(
179
+ `"${relativePath}" is a directory, not a file.`,
180
+ );
181
+ notAFile.status = 400;
182
+ throw notAFile;
183
+ }
184
+ throw err;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * The published library's path rule, refused as a TYPED 400.
190
+ *
191
+ * The library throws a bare `Error('Invalid path: …')` — it is shared with
192
+ * the frontend and cannot carry a backend domain type — and the routes used
193
+ * to recognise that by its message prefix. Converting it here, at the one
194
+ * layer that calls it, is what lets the route layer read a status off a
195
+ * class like it does for every other refusal.
196
+ */
197
+ function assertValidPath(relativePath: string): void {
198
+ const reason = validateRelativePath(relativePath);
199
+ if (reason) throw new WorkflowValidationError(`Invalid path: ${reason}`);
200
+ }
201
+
202
+ /** Throw the conditional write's 409 unless the file still holds `expectedContent`. */
203
+ async function assertConditionalWriteMatches(
204
+ absolutePath: string,
205
+ relativePath: string,
206
+ expectedContent: string,
207
+ ): Promise<void> {
208
+ const current = await readForConditionalWrite(absolutePath, relativePath);
209
+ if (current === expectedContent) return;
210
+ const stale: Error & { status?: number } = new Error(
211
+ `"${relativePath}" changed since you opened it. Reload it and apply your edit again, ` +
212
+ 'so the other change is not overwritten.',
213
+ );
214
+ stale.status = 409;
215
+ throw stale;
216
+ }
217
+
95
218
  /**
96
219
  * Per-directory read filter for the file tree. Given a batch of
97
220
  * workspace-relative entry paths (e.g. `staging-repo/Product/Knowledge/x.md`),
98
221
  * returns a verdict map keyed by those paths (`path → readable`). Injected by
99
222
  * the route from the access service so `WorkspaceService` stays access-agnostic
100
223
  * (it gets a function, not the access module). See `modules/access-model/kb-read-filter.ts`.
224
+ *
225
+ * `'unlisted'` hides an entry WITHOUT counting it as withheld: a listing
226
+ * decision (the admin-only `.bevelignore`) rather than content the caller's
227
+ * read rules keep from them, so it must not tell them something is being kept.
101
228
  */
102
- export type ReadTreeFilter = (wsRelPaths: string[]) => Promise<Map<string, boolean>>;
229
+ export type ReadTreeFilter = (wsRelPaths: string[]) => Promise<Map<string, boolean | 'unlisted'>>;
103
230
 
104
231
  export class WorkspaceService implements IWorkspaceService {
105
232
  /** Maps branch → absolute directory path. Lazily populated. */
@@ -117,6 +244,32 @@ export class WorkspaceService implements IWorkspaceService {
117
244
  /** Whether the last fetch per repo SUCCEEDED — read by strict callers of `ensureRemotesFetched`. */
118
245
  private readonly lastFetchOk = new Map<string, boolean>();
119
246
  private readonly inFlightFetches = new Map<string, Promise<void>>();
247
+ /**
248
+ * One mutation at a time per file, keyed by RESOLVED absolute path — so
249
+ * `x/a.md`, `./x/a.md` and `x//a.md` are one queue and not three. See
250
+ * {@link withPathTurn} for what takes a turn and why.
251
+ */
252
+ private readonly writeTurns = new Map<string, Promise<void>>();
253
+ /**
254
+ * The folder structure changes in flight, keyed by RESOLVED absolute folder
255
+ * path — see {@link withFolderTurn}.
256
+ */
257
+ private readonly folderTurns = new Map<string, Promise<void>>();
258
+ /**
259
+ * The turns the CURRENT async context already holds. Taking one it holds
260
+ * runs straight through instead of waiting for itself, which is what lets a
261
+ * caller wrap a whole read-decide-write sequence in a turn and still call
262
+ * the ordinary write inside it.
263
+ */
264
+ /**
265
+ * The turns the current async context holds, each with the tail of the
266
+ * nested mutations already launched inside it — so a re-entrant call never
267
+ * waits for the outer turn (that would hang) while two nested calls on the
268
+ * same path, launched side by side inside one turn, still run one at a
269
+ * time. The map is inherited by every child context; the entry object is
270
+ * shared, which is what makes siblings queue on one tail.
271
+ */
272
+ private readonly heldTurns = new AsyncLocalStorage<ReadonlyMap<string, HeldTurn>>();
120
273
 
121
274
  /**
122
275
  * Late-bound by the composition root — DiffService depends on
@@ -145,7 +298,15 @@ export class WorkspaceService implements IWorkspaceService {
145
298
  */
146
299
  kbRepoUrl: string | (() => string),
147
300
  private readonly kbDirName: string,
301
+ private readonly disk: ITreeWalker,
148
302
  gitUsername: string | (() => string) = 'x-access-token',
303
+ /**
304
+ * How git is run — see `shared/git.contract.ts`. The composition root
305
+ * passes the one runner carrying the deployment's deadline; the default
306
+ * is the same runner on its default deadline, for a directly constructed
307
+ * service.
308
+ */
309
+ private readonly gitRunner: IGitRunner = new NodeGitRunner(),
149
310
  ) {
150
311
  this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
151
312
  this.gitUsername = typeof gitUsername === 'function' ? gitUsername : () => gitUsername;
@@ -162,6 +323,77 @@ export class WorkspaceService implements IWorkspaceService {
162
323
  this.onWorkspaceCloned = listener;
163
324
  }
164
325
 
326
+ /**
327
+ * Every branch name this process has been shown to be real: named by a
328
+ * listing of origin's branches, or registered as a clone of ours. Together
329
+ * with the clones still on disk this is the whole of what the platform
330
+ * KNOWS about branch names — and the difference between "your link is to a
331
+ * branch that was deleted" (410) and "there is no branch by that name"
332
+ * (404).
333
+ *
334
+ * Names ACCUMULATE and are never removed, and the two halves of that are
335
+ * the same rule: the evidence we need is evidence about a branch that is
336
+ * no longer there. Replacing the set on each listing would forget a
337
+ * deleted branch at the moment its deletion becomes the thing we have to
338
+ * report; evicting an old name would silently turn a long-dead branch back
339
+ * into "never existed"; dropping a clone's name when its workspace is swept
340
+ * would do that to a branch we cloned ourselves. So nothing here is ever
341
+ * dropped. Growth is one short string per distinct branch name origin has
342
+ * ever shown this process — kilobytes for any real repository, and reset
343
+ * by a restart.
344
+ *
345
+ * In memory only, by the spec's decision — no new persistence. A restart
346
+ * forgets a deleted branch whose clone was already retired, and that name
347
+ * then reads as unknown, which is exactly what the platform then knows.
348
+ */
349
+ private readonly branchesEverHeardOf = new Set<string>();
350
+
351
+ /**
352
+ * Record the branch names a listing returned. Called by the git layer after
353
+ * every `listBranches` (wired in the composition root), which is the one
354
+ * place the platform ever sees origin's set of branches.
355
+ */
356
+ noteBranchesListed(names: readonly string[]): void {
357
+ for (const name of names) this.branchesEverHeardOf.add(name);
358
+ }
359
+
360
+ /**
361
+ * Register a branch's clone directory — the only writer of `branchDirs`, so
362
+ * that every clone this process opens (fresh, adopted off disk, or handed
363
+ * over by a concurrent bootstrap) also leaves its branch name in
364
+ * `branchesEverHeardOf`. `branchDirs` is a cache of live clones and loses the
365
+ * branch when its workspace is swept; the name left behind is the record
366
+ * that the branch was real, and outlives the clone.
367
+ */
368
+ private registerBranchDir(branch: string, workspaceDir: string): void {
369
+ this.branchDirs.set(branch, workspaceDir);
370
+ this.branchesEverHeardOf.add(branch);
371
+ }
372
+
373
+ /**
374
+ * Has the platform ever heard of this branch? A clone of it — registered in
375
+ * this process, remembered from one, or sitting on disk from a previous one
376
+ * — or a listing that named it. Only asked on the failure path, where
377
+ * origin has just answered that it has no such ref: a yes means the branch
378
+ * was deleted (410), a no means it never existed as far as the platform is
379
+ * concerned (404).
380
+ */
381
+ private async hasHeardOfBranch(branch: string): Promise<boolean> {
382
+ if (this.branchDirs.has(branch)) return true;
383
+ if (this.branchesEverHeardOf.has(branch)) return true;
384
+ try {
385
+ await fs.access(path.join(this.workspacesRoot, workspaceIdForBranch(branch), this.kbDirName, '.git'));
386
+ return true;
387
+ } catch (err) {
388
+ // Only "there is nothing there" answers the question. A permission or
389
+ // I/O fault means we could not look, and a storage failure of ours must
390
+ // not be reported to the caller as a branch that never existed: it
391
+ // stays the 500 the operator reads.
392
+ if (isAbsence(err)) return false;
393
+ throw err;
394
+ }
395
+ }
396
+
165
397
  /**
166
398
  * Build git clone args. Credentials go in as `clone --config`, which both
167
399
  * authenticates this clone and leaves the helper in the new repo's config for
@@ -248,6 +480,71 @@ export class WorkspaceService implements IWorkspaceService {
248
480
  return null;
249
481
  }
250
482
 
483
+ /**
484
+ * Every branch with a finished clone on disk, whether or not this process
485
+ * has touched it yet. The remote sync reads this: clones survive a restart,
486
+ * and a hook that fires before anyone opens a branch must still find its
487
+ * clone. Same disk scan as `findAnyWorkspaceId`.
488
+ *
489
+ * What is listed: directories holding a `<kbDirName>/.git`, whose name is
490
+ * the encoding of a branch git would accept, and whose bootstrap is not in
491
+ * flight right now. The in-flight exclusion matters because `git clone`
492
+ * creates `.git` long before the working tree is checked out — pulling a
493
+ * clone mid-bootstrap would run git against a half-written tree — and a
494
+ * clone that is being created this instant is current by definition. A
495
+ * clone left behind by a crash mid-checkout is NOT distinguished here: the
496
+ * rest of this service treats a directory with `.git` as a clone too, and
497
+ * the pull's own errors are how such a tree surfaces.
498
+ *
499
+ * A missing root means no clones (nothing has been bootstrapped yet); any
500
+ * other failure to read it is propagated, so a sync cannot report success
501
+ * over a root it could not see. One entry that cannot be probed (an ACL, a
502
+ * half-deleted directory) is returned with `unreadable` set rather than
503
+ * thrown, so it fails as one branch and not as the whole listing.
504
+ */
505
+ async listClonedWorkspaces(): Promise<Array<{ id: string; branch: string; unreadable?: string }>> {
506
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
507
+ try {
508
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
509
+ } catch (err) {
510
+ if (isAbsence(err)) return [];
511
+ throw err;
512
+ }
513
+ const cloned: Array<{ id: string; branch: string; unreadable?: string }> = [];
514
+ for (const entry of entries) {
515
+ if (!entry.isDirectory()) continue;
516
+ const branch = branchForWorkspaceId(entry.name);
517
+ // Not one of ours unless the name is the encoding of a branch git
518
+ // accepts: `branchForWorkspaceId` returns a malformed name unchanged, so
519
+ // the round-trip catches those, and the validator catches a stray
520
+ // directory whose name merely round-trips.
521
+ if (workspaceIdForBranch(branch) !== entry.name) continue;
522
+ try {
523
+ assertValidBranchName(branch);
524
+ } catch {
525
+ continue;
526
+ }
527
+ try {
528
+ await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
529
+ } catch (err) {
530
+ if (isAbsence(err)) continue;
531
+ cloned.push({
532
+ id: entry.name,
533
+ branch,
534
+ unreadable: `Could not read this branch's clone: ${redactError(err)}`,
535
+ });
536
+ continue;
537
+ }
538
+ // AFTER the probe, with no await in between: a bootstrap registers
539
+ // itself before it creates `.git`, so any clone whose `.git` was found
540
+ // while its bootstrap is running is in this map by now. Checking before
541
+ // the probe left a window where the clone started in between.
542
+ if (this.isBootstrapInFlight(branch)) continue;
543
+ cloned.push({ id: entry.name, branch });
544
+ }
545
+ return cloned;
546
+ }
547
+
251
548
  /**
252
549
  * Run the clone. When a `referenceRepo` is supplied and the referenced
253
550
  * clone fails (e.g. the sibling's object store is corrupt or mid-write),
@@ -259,24 +556,23 @@ export class WorkspaceService implements IWorkspaceService {
259
556
  branch: string,
260
557
  referenceRepo: string | null,
261
558
  ): Promise<void> {
559
+ // Long enough for a first clone, whatever the configured ceiling says.
560
+ const timeoutMs = Math.max(this.gitRunner.defaultTimeoutMs, CLONE_TIMEOUT_MS);
262
561
  if (referenceRepo) {
263
562
  try {
264
- await execFileAsync('git', this.gitCloneArgs(targetDir, branch, referenceRepo), {
265
- env: { ...process.env },
563
+ await this.gitRunner.run(path.dirname(targetDir), this.gitCloneArgs(targetDir, branch, referenceRepo), {
564
+ timeoutMs,
266
565
  });
267
566
  return;
268
567
  } catch (err) {
269
- console.warn(
270
- `[workspace] referenced clone for "${branch}" failed, retrying without reference:`,
271
- redactError(err),
272
- );
568
+ log.warn(`referenced clone for "${branch}" failed, retrying without reference:`, {
569
+ detail: redactError(err),
570
+ });
273
571
  // Clear any partial output so the retry clones into a clean dir.
274
572
  await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
275
573
  }
276
574
  }
277
- await execFileAsync('git', this.gitCloneArgs(targetDir, branch), {
278
- env: { ...process.env },
279
- });
575
+ await this.gitRunner.run(path.dirname(targetDir), this.gitCloneArgs(targetDir, branch), { timeoutMs });
280
576
  }
281
577
 
282
578
  /**
@@ -327,7 +623,7 @@ export class WorkspaceService implements IWorkspaceService {
327
623
  // pulls it. Once per branch per process — the cached paths above return
328
624
  // before reaching here.
329
625
  await this.normalizeCloneConfig(repoDir, branch);
330
- this.branchDirs.set(branch, workspaceDir);
626
+ this.registerBranchDir(branch, workspaceDir);
331
627
  return this.buildWorkspaceInfo(branch, workspaceDir);
332
628
  } catch {
333
629
  // Not on disk — bootstrap below.
@@ -346,7 +642,7 @@ export class WorkspaceService implements IWorkspaceService {
346
642
  const existingBootstrap = this.inFlightBootstraps.get(branch);
347
643
  if (existingBootstrap) {
348
644
  await existingBootstrap;
349
- this.branchDirs.set(branch, workspaceDir);
645
+ this.registerBranchDir(branch, workspaceDir);
350
646
  return this.buildWorkspaceInfo(branch, workspaceDir);
351
647
  }
352
648
  this.inFlightBootstraps.set(branch, bootstrap);
@@ -356,7 +652,7 @@ export class WorkspaceService implements IWorkspaceService {
356
652
  // naturally; seeding the remote is the KB startup phase's job, at boot.
357
653
  await fs.mkdir(workspaceDir, { recursive: true });
358
654
  await this.cloneProcessMapForBranch(workspaceDir, branch);
359
- this.branchDirs.set(branch, workspaceDir);
655
+ this.registerBranchDir(branch, workspaceDir);
360
656
  resolveBootstrap();
361
657
  } catch (err) {
362
658
  this.branchDirs.delete(branch);
@@ -408,6 +704,15 @@ export class WorkspaceService implements IWorkspaceService {
408
704
  }
409
705
  }
410
706
 
707
+ /**
708
+ * Whether a clone of `branch` is being created at this moment. Read by the
709
+ * retirement of a clone the host deleted: a bootstrap in flight means the
710
+ * branch exists again, so there is nothing stale to remove.
711
+ */
712
+ isBootstrapInFlight(branch: string): boolean {
713
+ return this.inFlightBootstraps.has(branch);
714
+ }
715
+
411
716
  /**
412
717
  * Bring an adopted clone's config back to what a fresh clone would carry.
413
718
  *
@@ -431,15 +736,12 @@ export class WorkspaceService implements IWorkspaceService {
431
736
  private async normalizeCloneConfig(repoDir: string, branch: string): Promise<void> {
432
737
  try {
433
738
  for (const args of cloneTrackingConfigArgs(branch)) {
434
- await execFileAsync('git', ['-C', repoDir, ...args]);
739
+ await this.gitRunner.run(repoDir, args);
435
740
  }
436
741
  } catch (err) {
437
742
  // One line per branch, not per key: every key writes to the same
438
743
  // `.git/config`, so what fails for one fails for all.
439
- console.warn(
440
- `[workspace] could not normalize the config of the "${branch}" clone:`,
441
- redactError(err),
442
- );
744
+ log.warn(`could not normalize the config of the "${branch}" clone:`, { detail: redactError(err) });
443
745
  }
444
746
  await this.stampCredentialHelper(repoDir, branch);
445
747
  }
@@ -480,28 +782,24 @@ export class WorkspaceService implements IWorkspaceService {
480
782
  // re-clone that fails on the same validation — masking the real cause.
481
783
  // Contain it as a loud non-stamp instead; `normalizeCloneConfig`'s
482
784
  // never-throws contract stays true.
483
- console.warn(
484
- `[workspace] refusing to stamp the credential helper of the "${branch}" clone:`,
485
- redactError(err),
486
- );
785
+ log.warn(`refusing to stamp the credential helper of the "${branch}" clone:`, { detail: redactError(err) });
487
786
  this.stampedCredentialFingerprint.delete(branch);
488
787
  return;
489
788
  }
490
789
  let stamped = true;
491
790
  for (const args of argLists) {
492
791
  try {
493
- await execFileAsync('git', ['-C', repoDir, ...args]);
792
+ await this.gitRunner.run(repoDir, args);
494
793
  } catch (err) {
495
794
  // `--unset-all` with no matching value (exit 5) is the expected no-op
496
795
  // for a clone that never carried an app helper; anything else is a
497
796
  // real failure.
498
- const code = (err as { code?: number } | null)?.code;
797
+ const code = (err as { exitCode?: number } | null)?.exitCode;
499
798
  if (!(args.includes('--unset-all') && code === 5)) {
500
799
  stamped = false;
501
- console.warn(
502
- `[workspace] could not stamp the credential helper of the "${branch}" clone:`,
503
- redactError(err),
504
- );
800
+ log.warn(`could not stamp the credential helper of the "${branch}" clone:`, {
801
+ detail: redactError(err),
802
+ });
505
803
  }
506
804
  }
507
805
  }
@@ -538,19 +836,25 @@ export class WorkspaceService implements IWorkspaceService {
538
836
  // propagate to the caller instead of getting silently swallowed by an
539
837
  // outer catch (the previous shape let an inner rethrow fall through
540
838
  // to a clone-into-non-empty-dir, producing a confusing 500).
541
- let targetExists = true;
542
- try {
543
- await fs.access(targetDir);
544
- } catch (err) {
545
- const code = (err as NodeJS.ErrnoException | null)?.code;
546
- if (code === 'ENOENT' || code === 'ENOTDIR') {
547
- targetExists = false;
548
- } else {
549
- throw err;
550
- }
839
+ //
840
+ // A FILE squatting the clone's name is refused outright, never wiped:
841
+ // the recovery below exists for a crashed bootstrap's directory shell,
842
+ // and a regular file here is a state a human put the deployment in — so
843
+ // it is named and stopped, the same stance the startup phase takes on a
844
+ // squatted reserved root. (Links are followed: one clone mounted
845
+ // elsewhere is the operator's business, as it is at the workspace root.)
846
+ const target = await fs.stat(targetDir).catch((err: unknown) => {
847
+ if (isAbsence(err)) return null;
848
+ throw err;
849
+ });
850
+ if (target !== null && !target.isDirectory()) {
851
+ throw new Error(
852
+ `The clone path "${this.kbDirName}" in this workspace exists but is not a directory. ` +
853
+ 'Remove or rename it — the platform requires this name to be the knowledge-base clone.',
854
+ );
551
855
  }
552
856
 
553
- if (targetExists) {
857
+ if (target !== null) {
554
858
  // The directory exists, but a previous bootstrap may have crashed
555
859
  // mid-clone — leaving a directory shell without `.git`. Treat
556
860
  // anything missing `.git` as not-cloned and re-clone. (Can't be
@@ -567,10 +871,9 @@ export class WorkspaceService implements IWorkspaceService {
567
871
  // re-clone". A transient EACCES / EIO / EBUSY must NOT delete
568
872
  // what might be a perfectly valid repo whose `.git` we couldn't
569
873
  // read this moment — surface the error so the caller can retry.
570
- const code = (err as NodeJS.ErrnoException | null)?.code;
571
- if (code !== 'ENOENT' && code !== 'ENOTDIR') {
572
- throw err;
573
- }
874
+ // (`targetDir` is known to be a directory here, so ENOTDIR can only
875
+ // mean `.git` itself vanished under us — an absence either way.)
876
+ if (!isAbsence(err)) throw err;
574
877
  }
575
878
  if (alreadyCloned) {
576
879
  // We don't auto-pull because that could clobber another user's
@@ -591,21 +894,18 @@ export class WorkspaceService implements IWorkspaceService {
591
894
  await this.runClone(targetDir, branch, reference);
592
895
  // Persist longpaths in the cloned repo's config so subsequent
593
896
  // checkouts also honor it.
594
- await execFileAsync('git', ['-C', targetDir, 'config', 'core.longpaths', 'true']);
897
+ await this.gitRunner.run(targetDir, ['config', 'core.longpaths', 'true']);
595
898
  // Generic bot identity for the clone — every workflow commit
596
899
  // overrides via `--author=…` so the real human shows up in
597
900
  // `git log`. This is purely the fallback committer.
598
- await execFileAsync('git', ['-C', targetDir, 'config', 'user.name', BOT_NAME]);
599
- await execFileAsync('git', ['-C', targetDir, 'config', 'user.email', BOT_EMAIL]);
901
+ await this.gitRunner.run(targetDir, ['config', 'user.name', BOT_NAME]);
902
+ await this.gitRunner.run(targetDir, ['config', 'user.email', BOT_EMAIL]);
600
903
  // Pin the clone to exactly one fetch refspec and one upstream ref for
601
904
  // this branch. `git clone -b` already produces that shape; stamping it
602
905
  // explicitly means the shape is asserted rather than assumed, and the
603
906
  // same call is what repairs an existing clone that drifted.
604
907
  await this.normalizeCloneConfig(targetDir, branch);
605
- console.log(
606
- `[workspace] Cloned ${this.kbDirName} for branch "${branch}"` +
607
- (reference ? ' (referenced a sibling clone)' : ''),
608
- );
908
+ log.info(`Cloned ${this.kbDirName} for branch "${branch}"` + (reference ? ' (referenced a sibling clone)' : ''));
609
909
  // A fresh clone has already downloaded every ref — tell the git layer
610
910
  // so the first `listBranches` skips the redundant implicit `git fetch`.
611
911
  // Isolated from the clone-rollback `catch` below: a misbehaving listener
@@ -613,16 +913,41 @@ export class WorkspaceService implements IWorkspaceService {
613
913
  try {
614
914
  this.onWorkspaceCloned?.(workspaceIdForBranch(branch));
615
915
  } catch (listenerErr) {
616
- console.error(
617
- `[workspace] onWorkspaceCloned listener failed for branch "${branch}":`,
618
- redactError(listenerErr),
619
- );
916
+ log.error(`onWorkspaceCloned listener failed for branch "${branch}":`, {
917
+ detail: redactError(listenerErr),
918
+ });
620
919
  }
621
920
  } catch (err) {
622
921
  const redacted = redactError(err);
623
- console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
624
922
  // Roll back partial state so the next bootstrap retries cleanly.
625
923
  await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
924
+ // A branch origin does not have is a fact about the branch, not a
925
+ // failure of ours — but WHICH fact depends on whether the platform ever
926
+ // knew the name. A branch it cloned or listed and origin no longer has
927
+ // was deleted (410, "this branch no longer exists"); a name it has
928
+ // never seen is simply not a branch (404), and saying "no longer exists
929
+ // on the remote" to a typo tells the reader it once did.
930
+ if (isMissingRemoteBranchFailure(redacted)) {
931
+ let everHeardOf: boolean;
932
+ try {
933
+ everHeardOf = await this.hasHeardOfBranch(branch);
934
+ } catch (probeErr) {
935
+ // We could not read our own storage, so we cannot say which of the
936
+ // two stories this is — and guessing either one states something
937
+ // about the branch that we do not know. Our failure, so: 500.
938
+ log.error(`Could not tell whether branch "${branch}" was ever known here:`, {
939
+ detail: redactError(probeErr),
940
+ });
941
+ throw new Error(`Failed to clone process map: ${redacted}`);
942
+ }
943
+ if (everHeardOf) {
944
+ log.info(`branch "${branch}" is gone from origin — nothing to clone`);
945
+ throw new RemoteBranchGoneError(branch);
946
+ }
947
+ log.info(`branch "${branch}" has never existed here — nothing to clone`);
948
+ throw new BranchNotFoundError(branch);
949
+ }
950
+ log.error(`Failed to clone for branch "${branch}":`, { detail: redacted });
626
951
  throw new Error(`Failed to clone process map: ${redacted}`);
627
952
  }
628
953
  }
@@ -650,23 +975,117 @@ export class WorkspaceService implements IWorkspaceService {
650
975
 
651
976
  async listFiles(workspaceId: string, readFilter?: ReadTreeFilter): Promise<FileTreeEntry> {
652
977
  const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
653
- return this.buildFileTree(workspaceDir, workspaceDir, BevelIgnoreStack.empty(), readFilter);
978
+ return this.buildFileTree(workspaceDir, workspaceDir, readFilter);
979
+ }
980
+
981
+ /** The repository inside a workspace directory: `<workspaceDir>/<kbDirName>`. */
982
+ private repoRoot(workspaceDir: string): string {
983
+ return path.join(workspaceDir, this.kbDirName);
654
984
  }
655
985
 
656
- async readFile(workspaceId: string, relativePath: string): Promise<string> {
986
+ /**
987
+ * THE resolution — the only place in this service that turns a workspace
988
+ * path into a location on disk.
989
+ *
990
+ * Two steps, and neither is optional. `normalizeWorkspacePath` places the
991
+ * path inside the repository (`KnowledgeBase/Report.md` is the page of that
992
+ * name in the checkout, not a stray beside it) and refuses `.`/`..`,
993
+ * backslashes and absolute paths outright. Then the RESOLVED path is checked
994
+ * against `<workspaceDir>/<kbDirName>` — after normalisation, because that
995
+ * is the path the operation will use, and a spelling that normalises inside
996
+ * and resolves outside is exactly the miss this check exists to catch.
997
+ *
998
+ * Callers use the returned `relativePath` for everything downstream — the
999
+ * git-internals check, the diff baseline, the lock-free path turns, their own
1000
+ * error messages — so what is checked is what is written. Nine inline
1001
+ * `path.resolve(workspaceDir, …)` calls used to do this, none of them
1002
+ * checking the repository root; a drift-guard test now fails if one returns.
1003
+ */
1004
+ private async resolveInsideRepo(
1005
+ workspaceId: string,
1006
+ wsPath: string,
1007
+ ): Promise<{ workspaceDir: string; relativePath: string; absolutePath: string }> {
657
1008
  const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
1009
+ const relativePath = normalizeWorkspacePath(wsPath, this.kbDirName);
658
1010
  const absolutePath = path.resolve(workspaceDir, relativePath);
659
- this.assertWithinWorkspace(absolutePath, workspaceDir);
1011
+ assertWithinDirectory(absolutePath, this.repoRoot(workspaceDir));
1012
+ return { workspaceDir, relativePath, absolutePath };
1013
+ }
1014
+
1015
+ async readFile(workspaceId: string, wsPath: string): Promise<string> {
1016
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1017
+ assertNoGitInternalsSegment(relativePath);
1018
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1019
+ await this.assertNotThroughLink(absolutePath, workspaceDir);
660
1020
  return fs.readFile(absolutePath, 'utf-8');
661
1021
  }
662
1022
 
663
- async readFileBinary(workspaceId: string, relativePath: string): Promise<Buffer> {
664
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
665
- const absolutePath = path.resolve(workspaceDir, relativePath);
666
- this.assertWithinWorkspace(absolutePath, workspaceDir);
1023
+ async readFileBinary(workspaceId: string, wsPath: string): Promise<Buffer> {
1024
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1025
+ assertNoGitInternalsSegment(relativePath);
1026
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1027
+ await this.assertNotThroughLink(absolutePath, workspaceDir);
667
1028
  return fs.readFile(absolutePath);
668
1029
  }
669
1030
 
1031
+ /**
1032
+ * Refuse a path that reaches its file through a symbolic link — as the
1033
+ * final component, or as any directory between the workspace root and it.
1034
+ * `resolveInsideRepo`'s containment is lexical: `knowledge-base/notes.md`
1035
+ * passes it however the name resolves, so a link the repository carries (they only
1036
+ * arrive by direct git push; the app's own writes never create one) would
1037
+ * let a read hand out bytes from anywhere the server process can read, and
1038
+ * a write replace whatever the link points at. The rule is the plugin
1039
+ * archive's: the deepest ancestor that exists must resolve to exactly its
1040
+ * own spelling under the workspace's resolved root — identity, not mere
1041
+ * containment — and the entry itself, when it exists, must not be a link.
1042
+ * The workspace root may sit behind a link of the operator's (a mounted
1043
+ * volume): both sides are resolved, so that is allowed.
1044
+ *
1045
+ * The same refusal as the lexical check on purpose: this IS a traversal,
1046
+ * reached through a link rather than through a spelling.
1047
+ */
1048
+ private async assertNotThroughLink(absolutePath: string, workspaceDir: string): Promise<void> {
1049
+ const traversal = () => new PathTraversalError();
1050
+ const entry = await fs.lstat(absolutePath).catch((err: unknown) => {
1051
+ if (isAbsence(err)) return null;
1052
+ throw err;
1053
+ });
1054
+ if (entry?.isSymbolicLink()) throw traversal();
1055
+ // The deepest existing ancestor: a write creates the rest, under it. Each
1056
+ // ancestor is lstat-ed BEFORE its absence is taken as leave to climb: a
1057
+ // dangling link resolves to nothing too, and climbing past it would let
1058
+ // a target created in the meantime receive the write.
1059
+ let ancestor = path.dirname(absolutePath);
1060
+ let realAncestor: string | null = null;
1061
+ while (realAncestor === null) {
1062
+ const ancestorEntry = await fs.lstat(ancestor).catch((err: unknown) => {
1063
+ if (isAbsence(err)) return null;
1064
+ throw err;
1065
+ });
1066
+ // The workspace directory itself may be a link of the operator's (one
1067
+ // workspace mounted elsewhere), like the root above it; the identity
1068
+ // check below still holds everything beneath it to its own spelling.
1069
+ if (ancestorEntry?.isSymbolicLink() && path.resolve(ancestor) !== path.resolve(workspaceDir)) throw traversal();
1070
+ realAncestor = await fs.realpath(ancestor).catch((err: unknown) => {
1071
+ if (isAbsence(err)) return null;
1072
+ throw err;
1073
+ });
1074
+ if (realAncestor === null) {
1075
+ const up = path.dirname(ancestor);
1076
+ if (up === ancestor) throw traversal();
1077
+ ancestor = up;
1078
+ }
1079
+ }
1080
+ const realRoot = await fs.realpath(workspaceDir);
1081
+ const expected = path.join(realRoot, path.relative(path.resolve(workspaceDir), ancestor));
1082
+ const same =
1083
+ process.platform === 'win32'
1084
+ ? realAncestor.toLowerCase() === expected.toLowerCase()
1085
+ : realAncestor === expected;
1086
+ if (!same) throw traversal();
1087
+ }
1088
+
670
1089
  /**
671
1090
  * Read every `.md` file under the KB repo in one disk pass, keyed by
672
1091
  * repo-root-relative POSIX path (e.g. `Product/NodeTypes/ServiceCommitment.md`)
@@ -681,23 +1100,15 @@ export class WorkspaceService implements IWorkspaceService {
681
1100
  const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
682
1101
  const repoRoot = path.join(workspaceDir, this.kbDirName);
683
1102
  const files: Record<string, string> = {};
684
- const walk = async (dir: string, ignoreStack: BevelIgnoreStack): Promise<void> => {
685
- const nextStack = await ignoreStack.extendedWith(dir);
686
- const entries = await fs.readdir(dir, { withFileTypes: true });
687
- for (const entry of entries) {
688
- if (entry.name === '.git' && entry.isDirectory()) continue;
689
- const childAbs = path.join(dir, entry.name);
690
- if (nextStack.isIgnored(childAbs, entry.isDirectory())) continue;
691
- if (entry.isDirectory()) {
692
- await walk(childAbs, nextStack);
693
- continue;
694
- }
695
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
696
- const repoPath = path.relative(repoRoot, childAbs).replace(/\\/g, '/');
697
- files[repoPath] = await fs.readFile(childAbs, 'utf-8');
698
- }
699
- };
700
- await walk(repoRoot, BevelIgnoreStack.empty());
1103
+ await this.disk.walk(repoRoot, explorerWalk(), [
1104
+ {
1105
+ async onFile(dir, name) {
1106
+ if (!name.endsWith('.md')) return;
1107
+ const repoPath = dir ? `${dir}/${name}` : name;
1108
+ files[repoPath] = await fs.readFile(path.join(repoRoot, dir, name), 'utf-8');
1109
+ },
1110
+ },
1111
+ ]);
701
1112
  return files;
702
1113
  }
703
1114
 
@@ -714,10 +1125,10 @@ export class WorkspaceService implements IWorkspaceService {
714
1125
  * size crosses `ZIP_DOWNLOAD_MAX_BYTES`. Buffered in memory (adm-zip has
715
1126
  * no streaming API); the cap therefore doubles as a peak-heap bound.
716
1127
  */
717
- async createFolderZip(workspaceId: string, relativePath: string): Promise<Buffer> {
718
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
719
- const absoluteRoot = path.resolve(workspaceDir, relativePath);
720
- this.assertWithinWorkspace(absoluteRoot, workspaceDir);
1128
+ async createFolderZip(workspaceId: string, wsPath: string): Promise<Buffer> {
1129
+ const { workspaceDir, relativePath, absolutePath: absoluteRoot } = await this.resolveInsideRepo(workspaceId, wsPath);
1130
+ assertNoGitInternalsSegment(relativePath);
1131
+ await assertNotGitInternals(workspaceDir, relativePath, absoluteRoot);
721
1132
  const stat = await fs.stat(absoluteRoot);
722
1133
  if (!stat.isDirectory()) {
723
1134
  throw new Error('Not a directory');
@@ -727,45 +1138,31 @@ export class WorkspaceService implements IWorkspaceService {
727
1138
  const zip = new AdmZip();
728
1139
  let totalBytes = 0;
729
1140
 
730
- const walk = async (dir: string, ignoreStack: BevelIgnoreStack): Promise<void> => {
731
- const nextStack = await ignoreStack.extendedWith(dir);
732
- const entries = await fs.readdir(dir, { withFileTypes: true });
733
- entries.sort((a, b) => a.name.localeCompare(b.name));
734
- for (const entry of entries) {
735
- if (entry.name === '.git' && entry.isDirectory()) continue;
736
- if (entry.name === '.gitkeep' && entry.isFile()) continue;
737
- const childAbs = path.join(dir, entry.name);
738
- if (nextStack.isIgnored(childAbs, entry.isDirectory())) continue;
739
- if (entry.isDirectory()) {
740
- await walk(childAbs, nextStack);
741
- continue;
742
- }
743
- if (!entry.isFile()) continue; // skip sockets, symlinks, etc.
744
- // Check the size cap BEFORE reading the file into memory. Reading
745
- // first would let a single hostile 2 GB file allocate the whole
746
- // buffer before the throw — defeating the cap as a peak-heap
747
- // bound. `stat.size` is an upper bound that we re-verify after
748
- // the read in case the file grew between stat and readFile.
749
- const stat = await fs.stat(childAbs);
750
- if (totalBytes + stat.size > ZIP_DOWNLOAD_MAX_BYTES) {
751
- throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
752
- }
753
- const data = await fs.readFile(childAbs);
754
- if (totalBytes + data.byteLength > ZIP_DOWNLOAD_MAX_BYTES) {
755
- throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
756
- }
757
- totalBytes += data.byteLength;
758
- // Path inside the archive: <folderName>/<relPathUnderFolder>, POSIX
759
- // separators regardless of host OS so the zip extracts cleanly
760
- // on Windows/Mac/Linux alike.
761
- const relInside = path
762
- .relative(absoluteRoot, childAbs)
763
- .replace(/\\/g, '/');
764
- zip.addFile(`${zipRoot}/${relInside}`, data);
765
- }
766
- };
767
-
768
- await walk(absoluteRoot, BevelIgnoreStack.empty());
1141
+ await this.disk.walk(absoluteRoot, explorerWalk(), [
1142
+ {
1143
+ async onFile(dir, name) {
1144
+ const childAbs = path.join(absoluteRoot, dir, name);
1145
+ // Check the size cap BEFORE reading the file into memory. Reading
1146
+ // first would let a single hostile 2 GB file allocate the whole
1147
+ // buffer before the throw — defeating the cap as a peak-heap
1148
+ // bound. `stat.size` is an upper bound that we re-verify after
1149
+ // the read in case the file grew between stat and readFile.
1150
+ const stat = await fs.stat(childAbs);
1151
+ if (totalBytes + stat.size > ZIP_DOWNLOAD_MAX_BYTES) {
1152
+ throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
1153
+ }
1154
+ const data = await fs.readFile(childAbs);
1155
+ if (totalBytes + data.byteLength > ZIP_DOWNLOAD_MAX_BYTES) {
1156
+ throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
1157
+ }
1158
+ totalBytes += data.byteLength;
1159
+ // Path inside the archive: <folderName>/<relPathUnderFolder>, POSIX
1160
+ // separators regardless of host OS so the zip extracts cleanly
1161
+ // on Windows/Mac/Linux alike.
1162
+ zip.addFile(`${zipRoot}/${dir ? `${dir}/${name}` : name}`, data);
1163
+ },
1164
+ },
1165
+ ]);
769
1166
  return zip.toBuffer();
770
1167
  }
771
1168
 
@@ -781,6 +1178,7 @@ export class WorkspaceService implements IWorkspaceService {
781
1178
  relativePath: string,
782
1179
  ): Promise<string | null> {
783
1180
  this.assertValidGitRef(ref);
1181
+ assertNoGitInternalsSegment(relativePath);
784
1182
  this.assertValidRepoRelativePath(relativePath);
785
1183
  const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
786
1184
  const repoDir = path.join(workspaceDir, this.kbDirName);
@@ -792,11 +1190,7 @@ export class WorkspaceService implements IWorkspaceService {
792
1190
 
793
1191
  for (const candidate of candidates) {
794
1192
  try {
795
- const { stdout } = await execFileAsync(
796
- 'git',
797
- ['-C', repoDir, 'show', `${candidate}:${relativePath}`],
798
- { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 },
799
- );
1193
+ const { stdout } = await this.gitRunner.run(repoDir, ['show', `${candidate}:${relativePath}`]);
800
1194
  return stdout;
801
1195
  } catch {
802
1196
  // Try next candidate.
@@ -809,15 +1203,25 @@ export class WorkspaceService implements IWorkspaceService {
809
1203
  * Run `git fetch --prune origin` for this branch's clone. Results are
810
1204
  * cached for `FETCH_CACHE_TTL_MS`; concurrent callers share the same
811
1205
  * in-flight fetch to avoid fetch storms when e.g. the CR list poll fans out.
1206
+ *
1207
+ * `force` skips the TTL — for a caller that KNOWS the remote just moved
1208
+ * (an event-driven `?fresh=1` read, which bypasses its own cache for the
1209
+ * same reason) and would otherwise answer from refs older than the change
1210
+ * it is being asked about. It keeps the in-flight join, which is the half
1211
+ * that actually prevents storms: ten forced callers in the same tick still
1212
+ * share one fetch.
812
1213
  */
813
- async ensureRemotesFetched(workspaceId: string, opts: { strict?: boolean } = {}): Promise<void> {
1214
+ async ensureRemotesFetched(
1215
+ workspaceId: string,
1216
+ opts: { strict?: boolean; force?: boolean } = {},
1217
+ ): Promise<void> {
814
1218
  const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
815
1219
  const repoDir = path.join(workspaceDir, this.kbDirName);
816
1220
  const now = Date.now();
817
1221
  // The TTL is stamped only by a SUCCESSFUL fetch, so a fresh window is
818
1222
  // itself the proof a strict caller wants.
819
1223
  const last = this.lastFetchAt.get(repoDir) ?? 0;
820
- if (now - last < FETCH_CACHE_TTL_MS) return;
1224
+ if (!opts.force && now - last < FETCH_CACHE_TTL_MS) return;
821
1225
 
822
1226
  const inFlight = this.inFlightFetches.get(repoDir);
823
1227
  if (inFlight) {
@@ -834,18 +1238,15 @@ export class WorkspaceService implements IWorkspaceService {
834
1238
  // This driver runs outside the git layer's per-workspace mutex, so it may
835
1239
  // only run the safe implicit-fetch shape — see `SAFE_IMPLICIT_FETCH_ARGS`
836
1240
  // in `kb-fs/clone-config.ts` for the full rationale.
837
- const promise = execFileAsync(
838
- 'git',
839
- ['-C', repoDir, ...SAFE_IMPLICIT_FETCH_ARGS],
840
- { env: { ...process.env } },
841
- )
1241
+ const promise = this.gitRunner
1242
+ .run(repoDir, [...SAFE_IMPLICIT_FETCH_ARGS])
842
1243
  .then(() => {
843
1244
  this.lastFetchAt.set(repoDir, Date.now());
844
1245
  this.lastFetchOk.set(repoDir, true);
845
1246
  })
846
1247
  .catch((err) => {
847
1248
  this.lastFetchOk.set(repoDir, false);
848
- console.warn('[workspace] git fetch origin failed:', redactError(err));
1249
+ log.warn('git fetch origin failed:', { detail: redactError(err) });
849
1250
  })
850
1251
  .finally(() => {
851
1252
  if (this.inFlightFetches.get(repoDir) === promise) {
@@ -873,12 +1274,20 @@ export class WorkspaceService implements IWorkspaceService {
873
1274
  }
874
1275
  }
875
1276
 
876
- async deleteFile(workspaceId: string, relativePath: string): Promise<void> {
877
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
878
- const absolutePath = path.resolve(workspaceDir, relativePath);
879
- this.assertWithinWorkspace(absolutePath, workspaceDir);
880
- await fs.rm(absolutePath, { recursive: true, force: true });
881
- await this.diffService?.markUserDeleted(workspaceId, relativePath);
1277
+ async deleteFile(workspaceId: string, wsPath: string): Promise<void> {
1278
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1279
+ assertNoGitInternalsSegment(relativePath);
1280
+ // Under the path's turn like every other single-file mutation: a delete
1281
+ // landing between a conditional write's compare and its write would let
1282
+ // that write recreate the file the delete had just removed. The diff
1283
+ // baseline is updated inside the same turn, so two mutations of one path
1284
+ // update it in the order they landed on disk. The resolved git check runs
1285
+ // inside the turn too (see `withPathTurn`), so it cannot reorder callers.
1286
+ await this.withResolvedPathTurn(absolutePath, async () => {
1287
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1288
+ await fs.rm(absolutePath, { recursive: true, force: true });
1289
+ await this.diffService?.markUserDeleted(workspaceId, relativePath);
1290
+ });
882
1291
  }
883
1292
 
884
1293
  /**
@@ -900,74 +1309,345 @@ export class WorkspaceService implements IWorkspaceService {
900
1309
  await fs.rm(tmpDir, { recursive: true, force: true });
901
1310
  }
902
1311
 
903
- async moveEntry(workspaceId: string, oldRelativePath: string, newRelativePath: string): Promise<void> {
904
- assertValidRelativePath(newRelativePath);
905
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
906
- const oldAbsolute = path.resolve(workspaceDir, oldRelativePath);
907
- const newAbsolute = path.resolve(workspaceDir, newRelativePath);
908
- this.assertWithinWorkspace(oldAbsolute, workspaceDir);
909
- this.assertWithinWorkspace(newAbsolute, workspaceDir);
1312
+ /**
1313
+ * Rename or move one entry.
1314
+ *
1315
+ * Deliberately NOT under {@link withPathTurn}: a move mutates two paths, and
1316
+ * holding both turns would introduce the one shape that can deadlock here
1317
+ * (a move waiting on a target whose holder is waiting on the move's other
1318
+ * path). A rename onto a path being conditionally written is therefore
1319
+ * outside that guarantee; the workflow lock on the moved path is what
1320
+ * serializes it against the app's own editors.
1321
+ */
1322
+ async moveEntry(workspaceId: string, oldWsPath: string, newWsPath: string): Promise<void> {
1323
+ const { workspaceDir, relativePath: oldRelativePath, absolutePath: oldAbsolute } =
1324
+ await this.resolveInsideRepo(workspaceId, oldWsPath);
1325
+ const { relativePath: newRelativePath, absolutePath: newAbsolute } =
1326
+ await this.resolveInsideRepo(workspaceId, newWsPath);
1327
+ // A move and a rename are the same call, so this is where the checkout's
1328
+ // own name is reserved at the repository root for both.
1329
+ assertRepoRootNameFree(newRelativePath, this.kbDirName);
1330
+ assertNoGitInternalsSegment(oldRelativePath);
1331
+ assertNoGitInternalsSegment(newRelativePath);
1332
+ assertValidPath(newRelativePath);
1333
+ await assertNotGitInternals(workspaceDir, oldRelativePath, oldAbsolute);
1334
+ await assertNotGitInternals(workspaceDir, newRelativePath, newAbsolute);
1335
+ // Both ends: a link on the way to either end would carry the rename
1336
+ // outside. A link used AS the source is refused too — a committed link
1337
+ // is not something the app moves around, any more than reads it.
1338
+ await this.assertNotThroughLink(oldAbsolute, workspaceDir);
1339
+ await this.assertNotThroughLink(newAbsolute, workspaceDir);
1340
+ // Before `mkdir`, so a refused move leaves no empty folder behind — and
1341
+ // it is the look that produces the sentence naming what is in the way.
1342
+ await this.assertDestinationFree(oldAbsolute, newAbsolute, newRelativePath);
910
1343
  await fs.mkdir(path.dirname(newAbsolute), { recursive: true });
911
- await fs.rename(oldAbsolute, newAbsolute);
1344
+ // The move itself cannot replace anything either, so a destination that
1345
+ // appears between the look above and this line is refused rather than
1346
+ // eaten (`shared/rename-no-replace.ts`). This path takes no lock on
1347
+ // purpose — see the note above — so the atomicity has to come from the
1348
+ // filesystem call.
1349
+ try {
1350
+ await renameNoReplace(oldAbsolute, newAbsolute, newRelativePath);
1351
+ } catch (err) {
1352
+ if (err instanceof DestinationTakenError) {
1353
+ throw new EntryExistsError(err.entryKind, newRelativePath);
1354
+ }
1355
+ throw err;
1356
+ }
912
1357
  if (this.diffService) {
913
1358
  await this.diffService.markUserDeleted(workspaceId, oldRelativePath);
914
1359
  await this.diffService.syncFromDisk(workspaceId, newRelativePath);
915
1360
  }
916
1361
  }
917
1362
 
1363
+ /**
1364
+ * Refuse a move onto a name that is taken. `fs.rename` REPLACES an existing
1365
+ * file on every platform — that is how renaming a `.docx` onto an existing
1366
+ * `.md` handed the markdown file the Word bytes — so the destination is
1367
+ * looked at first, and a move never overwrites a file or merges into a
1368
+ * folder.
1369
+ *
1370
+ * Judged by `inspectDestination`, which reads a case-only rename on a
1371
+ * case-insensitive filesystem — where `notes.md` → `Notes.md` finds the
1372
+ * SOURCE at the destination — as the rename that was asked for rather than a
1373
+ * clash, and everything else as a clash. It is the same reading the move
1374
+ * itself uses, so the sentence and the refusal cannot drift apart.
1375
+ */
1376
+ private async assertDestinationFree(
1377
+ oldAbsolute: string,
1378
+ newAbsolute: string,
1379
+ newRelativePath: string,
1380
+ ): Promise<void> {
1381
+ const verdict = await inspectDestination(oldAbsolute, newAbsolute);
1382
+ if (verdict.state === 'taken') throw new EntryExistsError(verdict.kind, newRelativePath);
1383
+ }
1384
+
1385
+ /**
1386
+ * Refuse a conditional write, with the same 409 {@link writeFile} throws,
1387
+ * without writing anything. The route calls this BEFORE the creator-access
1388
+ * plan: that plan can commit a seeded `access.md` under its own lock, and a
1389
+ * precondition failing after it would leave an authorization grant behind
1390
+ * for a save that never landed. The write's own compare, under its write
1391
+ * turn, stays the authoritative one; this only keeps the refusal free of
1392
+ * side effects.
1393
+ */
1394
+ async assertContentMatches(
1395
+ workspaceId: string,
1396
+ wsPath: string,
1397
+ expectedContent: string,
1398
+ ): Promise<void> {
1399
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1400
+ assertNoGitInternalsSegment(relativePath);
1401
+ assertValidPath(relativePath);
1402
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1403
+ await assertConditionalWriteMatches(absolutePath, relativePath,expectedContent);
1404
+ }
1405
+
1406
+ /**
1407
+ * Run `op` as the only mutation in flight for `relativePath` in this
1408
+ * process, so a read-then-decide-then-write sequence inside it cannot have
1409
+ * another mutation land in the middle of it.
1410
+ *
1411
+ * The conditional write ({@link writeFile}'s `expectedContent`) needs that,
1412
+ * and so does a caller whose decision spans more than one call: `PUT /file`
1413
+ * checks the precondition, plans a creator-access grant that COMMITS, and
1414
+ * only then writes, and a save that lands in between would leave that grant
1415
+ * behind for a write about to be refused. Such a caller wraps the sequence
1416
+ * in a turn and the write inside it takes the same one, re-entrantly.
1417
+ *
1418
+ * The route's own per-path lock does not establish this: it deliberately
1419
+ * runs the op WITHOUT acquiring when the caller already holds the lock, so
1420
+ * two requests from that holder would interleave.
1421
+ *
1422
+ * The reach of the guarantee, exactly: within this process, one mutation at
1423
+ * a time per resolved path. ACROSS instances the workflow lock row is the
1424
+ * coordinator, and it agrees with this queue only because `PUT /file`
1425
+ * canonicalises the path before taking either (see `canonicalRelativePath`);
1426
+ * a caller that reaches the service directly with an odd spelling gets the
1427
+ * local queue and whatever lock it took itself. Case is not folded: on the
1428
+ * Linux deployment target `Foo.md` and `foo.md` are two files, and treating
1429
+ * them as one would be a worse bug than the race it would close.
1430
+ */
1431
+ async withPathTurn<T>(workspaceId: string, wsPath: string, op: () => Promise<T>): Promise<T> {
1432
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1433
+ assertNoGitInternalsSegment(relativePath);
1434
+ assertValidPath(relativePath);
1435
+ // The resolved check reads the disk, and how long that takes depends on
1436
+ // the spelling. Run before the queue, it let a later call overtake an
1437
+ // earlier one for the same file; inside the turn, callers keep their order
1438
+ // and `op` still never runs on a path that resolves into the git folder.
1439
+ return this.withResolvedPathTurn(absolutePath, async () => {
1440
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1441
+ return op();
1442
+ });
1443
+ }
1444
+
1445
+ /**
1446
+ * One folder structure change at a time per SUBTREE: an explicit folder
1447
+ * delete and the placeholder that keeps an emptied folder. Two turns wait
1448
+ * for each other when one folder is, or sits inside, the other, so keeping
1449
+ * a folder can never run in the middle of deleting it (and write the
1450
+ * placeholder back into a folder whose delete already enumerated its
1451
+ * files), and a delete never starts while a folder under it is being kept.
1452
+ * Not re-entrant: take it once, and never around another folder's turn.
1453
+ *
1454
+ * The reach of the guarantee, like {@link withPathTurn}'s: within this
1455
+ * process, over this process's clones. Across instances the workflow lock
1456
+ * rows coordinate — the placeholder write takes its own path's lock, as the
1457
+ * folder delete takes each file's — and each clone's changes meet in git,
1458
+ * as any write into a folder another instance deletes already does.
1459
+ */
1460
+ async withFolderTurn<T>(workspaceId: string, wsDir: string, op: () => Promise<T>): Promise<T> {
1461
+ const { relativePath: relativeDir, absolutePath: absoluteDir } = await this.resolveInsideRepo(workspaceId, wsDir);
1462
+ assertValidPath(relativeDir);
1463
+ const overlaps = (held: string) =>
1464
+ held === absoluteDir ||
1465
+ held.startsWith(absoluteDir + path.sep) ||
1466
+ absoluteDir.startsWith(held + path.sep);
1467
+ // Check-and-claim with no await in between, so two callers cannot both
1468
+ // find the subtree free.
1469
+ for (;;) {
1470
+ const waiting = [...this.folderTurns].filter(([held]) => overlaps(held)).map(([, turn]) => turn);
1471
+ if (waiting.length === 0) break;
1472
+ await Promise.all(waiting);
1473
+ }
1474
+ let release!: () => void;
1475
+ this.folderTurns.set(absoluteDir, new Promise<void>((resolve) => (release = resolve)));
1476
+ try {
1477
+ return await op();
1478
+ } finally {
1479
+ this.folderTurns.delete(absoluteDir);
1480
+ release();
1481
+ }
1482
+ }
1483
+
1484
+ /**
1485
+ * {@link withPathTurn} on an already-resolved path.
1486
+ *
1487
+ * A turn this context holds runs through: waiting for itself would hang.
1488
+ * What is stored is the SETTLED tail, never the caller's promise, so a
1489
+ * throwing mutation cannot wedge the queue behind it and no rejection is
1490
+ * left unhandled; the entry is dropped once it is still the tail, so the
1491
+ * map does not grow by an entry per file ever touched.
1492
+ *
1493
+ * Nesting a DIFFERENT file's turn inside one is safe here because the only
1494
+ * caller that does it is `PUT /file`, and the grant it seeds is always an
1495
+ * `access.md` at or above its target: two such sequences cannot each hold
1496
+ * what the other wants, since that would need each target to sit above the
1497
+ * other. Every other holder takes one turn and nests nothing.
1498
+ */
1499
+ private async withResolvedPathTurn<T>(absolutePath: string, op: () => Promise<T>): Promise<T> {
1500
+ const held = this.heldTurns.getStore();
1501
+ const inner = held?.get(absolutePath);
1502
+ if (inner) {
1503
+ // Re-entrant: runs without waiting for the turn this context holds —
1504
+ // but behind any sibling launched inside that same turn, so a held
1505
+ // turn that fires two conditional writes at once cannot have both
1506
+ // compare the same old content and then overwrite each other.
1507
+ const turn = inner.tail.then(() => op());
1508
+ inner.tail = turn.then(
1509
+ () => undefined,
1510
+ () => undefined,
1511
+ );
1512
+ return turn;
1513
+ }
1514
+ const run = (): Promise<T> =>
1515
+ this.heldTurns.run(new Map([...(held ?? []), [absolutePath, { tail: Promise.resolve() }]]), op);
1516
+ const previous = this.writeTurns.get(absolutePath);
1517
+ const turn = previous ? previous.then(run) : run();
1518
+ const tail = turn.then(
1519
+ () => undefined,
1520
+ () => undefined,
1521
+ );
1522
+ this.writeTurns.set(absolutePath, tail);
1523
+ void tail.then(() => {
1524
+ if (this.writeTurns.get(absolutePath) === tail) this.writeTurns.delete(absolutePath);
1525
+ });
1526
+ return turn;
1527
+ }
1528
+
1529
+ /**
1530
+ * Write one workspace file.
1531
+ *
1532
+ * - `failIfExists` is an exclusive create (see the `wx` note below).
1533
+ * - `expectedContent` is a conditional write: the file must still hold
1534
+ * exactly that text, or the write is refused with a 409. It is what lets
1535
+ * an editor that composed its save from a snapshot (the inline agent
1536
+ * description merges the private comments it read minutes earlier) refuse
1537
+ * rather than erase whatever landed in between. An absent file reads as
1538
+ * the empty string, so `expectedContent: ''` means "expect nothing there
1539
+ * yet" and creates it.
1540
+ *
1541
+ * The compare and the write run inside this path's write turn, so no other
1542
+ * write through this service can land between them, whatever locking the
1543
+ * caller did or skipped.
1544
+ */
918
1545
  async writeFile(
919
1546
  workspaceId: string,
920
- relativePath: string,
1547
+ wsPath: string,
921
1548
  content: string,
922
- options?: { failIfExists?: boolean },
1549
+ options?: { failIfExists?: boolean; expectedContent?: string },
923
1550
  ): Promise<void> {
924
- assertValidRelativePath(relativePath);
925
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
926
- const absolutePath = path.resolve(workspaceDir, relativePath);
927
- this.assertWithinWorkspace(absolutePath, workspaceDir);
928
- await fs.mkdir(path.dirname(absolutePath), { recursive: true });
929
- try {
930
- // `wx` makes create-if-absent ATOMIC at the fs level — an exists-check
931
- // followed by a plain write would let two concurrent creators (or a
932
- // stale client whose file list predates the file) both pass the check
933
- // and silently replace each other's content.
934
- await fs.writeFile(absolutePath, content, {
935
- encoding: 'utf-8',
936
- flag: options?.failIfExists ? 'wx' : 'w',
937
- });
938
- } catch (err) {
939
- if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
940
- const conflict: Error & { status?: number } = new Error(
941
- `"${relativePath}" already exists.`,
942
- );
943
- conflict.status = 409;
944
- throw conflict;
1551
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1552
+ // A write creates the folders on the way to it, so the reserved root name
1553
+ // is refused here too — otherwise `knowledge-base/knowledge-base/x.md`
1554
+ // would bring the unreachable folder into existence around the file.
1555
+ assertRepoRootNameFree(relativePath, this.kbDirName);
1556
+ assertNoGitInternalsSegment(relativePath);
1557
+ assertValidPath(relativePath);
1558
+ await this.withResolvedPathTurn(absolutePath, async () => {
1559
+ // Inside the turn, beside the link check (see `withPathTurn`).
1560
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1561
+ await this.assertNotThroughLink(absolutePath, workspaceDir);
1562
+ // Compare BEFORE creating anything. The parent chain used to be made
1563
+ // first, so a refused save at `x/new-folder/note.md` left an empty
1564
+ // `x/new-folder/` behind in everyone's tree — for a write that never
1565
+ // happened. The compare reads the file, and an absent file reads as
1566
+ // empty whether or not its directory exists.
1567
+ if (options?.expectedContent !== undefined) {
1568
+ await assertConditionalWriteMatches(absolutePath, relativePath,options.expectedContent);
945
1569
  }
946
- throw err;
947
- }
948
- await this.diffService?.syncFromDisk(workspaceId, relativePath);
1570
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
1571
+ try {
1572
+ // `wx` makes create-if-absent ATOMIC at the fs level — an exists-check
1573
+ // followed by a plain write would let two concurrent creators (or a
1574
+ // stale client whose file list predates the file) both pass the check
1575
+ // and silently replace each other's content.
1576
+ await fs.writeFile(absolutePath, content, {
1577
+ encoding: 'utf-8',
1578
+ flag: options?.failIfExists ? 'wx' : 'w',
1579
+ });
1580
+ } catch (err) {
1581
+ if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
1582
+ const conflict: Error & { status?: number } = new Error(
1583
+ `"${relativePath}" already exists.`,
1584
+ );
1585
+ conflict.status = 409;
1586
+ throw conflict;
1587
+ }
1588
+ throw err;
1589
+ }
1590
+ // Inside the turn, so the baseline follows the order the writes landed in.
1591
+ await this.diffService?.syncFromDisk(workspaceId, relativePath);
1592
+ });
949
1593
  }
950
1594
 
951
- async createDirectory(workspaceId: string, relativePath: string): Promise<void> {
952
- assertValidRelativePath(relativePath);
953
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
954
- const absolutePath = path.resolve(workspaceDir, relativePath);
955
- this.assertWithinWorkspace(absolutePath, workspaceDir);
1595
+ async createDirectory(workspaceId: string, wsPath: string): Promise<void> {
1596
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1597
+ assertRepoRootNameFree(relativePath, this.kbDirName);
1598
+ assertNoGitInternalsSegment(relativePath);
1599
+ assertValidPath(relativePath);
1600
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1601
+ await this.assertNotThroughLink(absolutePath, workspaceDir);
956
1602
  await fs.mkdir(absolutePath, { recursive: true });
957
1603
  const entries = await fs.readdir(absolutePath);
958
1604
  if (entries.length === 0) {
959
- await fs.writeFile(path.join(absolutePath, '.gitkeep'), '', 'utf-8');
1605
+ await fs.writeFile(path.join(absolutePath, FOLDER_PLACEHOLDER), '', 'utf-8');
960
1606
  }
961
1607
  }
962
1608
 
963
- async writeFileBinary(workspaceId: string, relativePath: string, data: Uint8Array): Promise<void> {
964
- assertValidRelativePath(relativePath);
965
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
966
- const absolutePath = path.resolve(workspaceDir, relativePath);
967
- this.assertWithinWorkspace(absolutePath, workspaceDir);
968
- await fs.mkdir(path.dirname(absolutePath), { recursive: true });
969
- await fs.writeFile(absolutePath, data);
970
- await this.diffService?.syncFromDisk(workspaceId, relativePath);
1609
+ /**
1610
+ * Write the empty-folder placeholder into `relativeDir` when that folder
1611
+ * exists and holds nothing; true when it wrote one. Unlike
1612
+ * {@link createDirectory} it never creates the folder: one that is gone was
1613
+ * deleted explicitly and stays gone. A folder reached through a link is
1614
+ * refused like any write through one, so the placeholder cannot land
1615
+ * outside the workspace.
1616
+ */
1617
+ async writeFolderPlaceholder(workspaceId: string, wsDir: string): Promise<boolean> {
1618
+ const { workspaceDir, relativePath: relativeDir, absolutePath: absoluteDir } =
1619
+ await this.resolveInsideRepo(workspaceId, wsDir);
1620
+ assertValidPath(relativeDir);
1621
+ const placeholder = path.join(absoluteDir, FOLDER_PLACEHOLDER);
1622
+ await this.assertNotThroughLink(placeholder, workspaceDir);
1623
+ let entries: string[];
1624
+ try {
1625
+ entries = await fs.readdir(absoluteDir);
1626
+ } catch (err) {
1627
+ if (isAbsence(err)) return false;
1628
+ throw err;
1629
+ }
1630
+ if (entries.length > 0) return false;
1631
+ await fs.writeFile(placeholder, '', { flag: 'wx' });
1632
+ return true;
1633
+ }
1634
+
1635
+ async writeFileBinary(workspaceId: string, wsPath: string, data: Uint8Array): Promise<void> {
1636
+ const { workspaceDir, relativePath, absolutePath } = await this.resolveInsideRepo(workspaceId, wsPath);
1637
+ assertRepoRootNameFree(relativePath, this.kbDirName);
1638
+ assertNoGitInternalsSegment(relativePath);
1639
+ assertValidPath(relativePath);
1640
+ // An upload over a path is a mutation of that path, so it takes the same
1641
+ // turn as a text write: see {@link withPathTurn}. The whole of it — the
1642
+ // parent chain too, so a folder delete serialized before this turn
1643
+ // cannot remove the parent between its creation and the write.
1644
+ await this.withResolvedPathTurn(absolutePath, async () => {
1645
+ await assertNotGitInternals(workspaceDir, relativePath, absolutePath);
1646
+ await this.assertNotThroughLink(absolutePath, workspaceDir);
1647
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
1648
+ await fs.writeFile(absolutePath, data);
1649
+ await this.diffService?.syncFromDisk(workspaceId, relativePath);
1650
+ });
971
1651
  }
972
1652
 
973
1653
  /**
@@ -978,8 +1658,8 @@ export class WorkspaceService implements IWorkspaceService {
978
1658
  */
979
1659
  async unzipFile(
980
1660
  workspaceId: string,
981
- zipRelativePath: string,
982
- destDirRelativePath?: string,
1661
+ zipWsPath: string,
1662
+ destDirWsPath?: string,
983
1663
  /**
984
1664
  * Per-extracted-path write guard (ontology-session boundary). Called with
985
1665
  * each entry's workspace-relative target before it is written; if it throws,
@@ -988,28 +1668,56 @@ export class WorkspaceService implements IWorkspaceService {
988
1668
  */
989
1669
  guardWrite?: (wsRelativePath: string) => Promise<void>,
990
1670
  ): Promise<UnzipResult> {
991
- const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
992
- const zipAbsolute = path.resolve(workspaceDir, zipRelativePath);
993
- this.assertWithinWorkspace(zipAbsolute, workspaceDir);
1671
+ const { workspaceDir, relativePath: zipRelativePath, absolutePath: zipAbsolute } =
1672
+ await this.resolveInsideRepo(workspaceId, zipWsPath);
1673
+ assertNoGitInternalsSegment(zipRelativePath);
1674
+ await assertNotGitInternals(workspaceDir, zipRelativePath, zipAbsolute);
994
1675
  if (!zipRelativePath.toLowerCase().endsWith('.zip')) {
995
- throw new Error('Only .zip files can be extracted');
1676
+ throw new WorkflowValidationError('Only .zip files can be extracted');
996
1677
  }
997
1678
 
998
- const inferredDest = (() => {
999
- const d = path.posix.dirname(zipRelativePath.replace(/\\/g, '/'));
1000
- return d === '.' ? '' : d;
1001
- })();
1002
- const destRel = destDirRelativePath ?? inferredDest;
1003
- if (destRel) assertValidRelativePath(destRel);
1004
- const destAbsolute = destRel ? path.resolve(workspaceDir, destRel) : workspaceDir;
1005
- this.assertWithinWorkspace(destAbsolute, workspaceDir);
1679
+ // Inferred from the NORMALISED archive path, so an archive named without
1680
+ // the prefix extracts beside itself inside the repository rather than at
1681
+ // the workspace root.
1682
+ const inferredDest = path.posix.dirname(zipRelativePath);
1683
+ const { relativePath: destRel, absolutePath: destAbsolute } =
1684
+ await this.resolveInsideRepo(workspaceId, destDirWsPath ?? inferredDest);
1685
+ assertValidPath(destRel);
1686
+ assertNoGitInternalsSegment(destRel);
1687
+ await assertNotGitInternals(workspaceDir, destRel, destAbsolute);
1688
+ // Neither the archive nor the destination may sit behind a link; each
1689
+ // entry's own target is checked again below, once it is known.
1690
+ await this.assertNotThroughLink(zipAbsolute, workspaceDir);
1691
+ if (destAbsolute !== this.repoRoot(workspaceDir)) await this.assertNotThroughLink(destAbsolute, workspaceDir);
1692
+
1693
+ // The archive is READ ONCE, here, and the reader is handed those bytes —
1694
+ // it is never given the path to open for itself. Two reasons, and the
1695
+ // first is why this is not a `stat` followed by `new AdmZip(path)`:
1696
+ //
1697
+ // - The zip reader cannot tell absence from corruption. On a path with
1698
+ // nothing at it `AdmZip` throws "ADM-ZIP: Invalid filename", which would
1699
+ // be dressed up as an unreadable archive (422) and blame the bytes for
1700
+ // a name that never existed. Only the read knows which it was.
1701
+ // - A separate probe answers about a DIFFERENT moment than the open. An
1702
+ // archive deleted in between passed the probe and then failed the open,
1703
+ // and the caller got the 422 the probe existed to prevent. One read is
1704
+ // one moment, so there is no in-between to lose.
1705
+ //
1706
+ // `AdmZip` from a buffer costs nothing extra: given a path it reads the
1707
+ // whole file in anyway.
1708
+ let zipBytes: Buffer;
1709
+ try {
1710
+ zipBytes = await fs.readFile(zipAbsolute);
1711
+ } catch (err) {
1712
+ if (isAbsence(err)) throw new PathNotFoundError(zipRelativePath);
1713
+ throw err;
1714
+ }
1006
1715
 
1007
1716
  let zip: AdmZip;
1008
1717
  try {
1009
- zip = new AdmZip(zipAbsolute);
1718
+ zip = new AdmZip(zipBytes);
1010
1719
  } catch (err) {
1011
- const msg = err instanceof Error ? err.message : String(err);
1012
- throw new Error(`Could not read zip file: ${msg}`);
1720
+ throw new UnreadableArchiveError(err instanceof Error ? err.message : String(err));
1013
1721
  }
1014
1722
 
1015
1723
  // The destination directory is created on demand by the first allowed entry
@@ -1046,6 +1754,12 @@ export class WorkspaceService implements IWorkspaceService {
1046
1754
  continue;
1047
1755
  }
1048
1756
 
1757
+ // An archive never writes into the git folder, whatever the entry's spelling.
1758
+ if (hasGitInternalsSegment(rawName)) {
1759
+ skipped.push({ path: rawName, reason: new GitInternalsError().message });
1760
+ continue;
1761
+ }
1762
+
1049
1763
  const trimmed = rawName.replace(/\/+$/, '');
1050
1764
  const segments = trimmed.split('/').filter((s) => s.length > 0);
1051
1765
  let invalidReason: string | null = null;
@@ -1086,6 +1800,38 @@ export class WorkspaceService implements IWorkspaceService {
1086
1800
  }
1087
1801
  };
1088
1802
 
1803
+ // The symbolic-link rule, per entry: a link already on disk under the
1804
+ // destination must not redirect this entry's bytes. Reported like the
1805
+ // other per-entry refusals, so one such entry does not fail the rest.
1806
+ // The checkout's own name is reserved at the repository root here too: an
1807
+ // archive carrying it would otherwise create the one folder no workspace
1808
+ // path can name. Reported like the other per-entry refusals — one such
1809
+ // entry does not fail the rest — and asked on its own rather than inside
1810
+ // the catch below, which stays the closed list of types it was.
1811
+ const reservedName = (() => {
1812
+ try {
1813
+ assertRepoRootNameFree(relForReport, this.kbDirName);
1814
+ return null;
1815
+ } catch (err) {
1816
+ return err instanceof Error ? err.message : String(err);
1817
+ }
1818
+ })();
1819
+ if (reservedName !== null) {
1820
+ skipped.push({ path: rawName, reason: reservedName });
1821
+ continue;
1822
+ }
1823
+
1824
+ try {
1825
+ await assertNotGitInternals(workspaceDir, trimmed, targetAbsolute);
1826
+ await this.assertNotThroughLink(targetAbsolute, workspaceDir);
1827
+ } catch (err) {
1828
+ if (err instanceof PathTraversalError || err instanceof GitInternalsError) {
1829
+ skipped.push({ path: rawName, reason: err.message });
1830
+ continue;
1831
+ }
1832
+ throw err;
1833
+ }
1834
+
1089
1835
  if (entry.isDirectory) {
1090
1836
  if (!(await allowEntry())) continue;
1091
1837
  await fs.mkdir(targetAbsolute, { recursive: true });
@@ -1224,15 +1970,9 @@ export class WorkspaceService implements IWorkspaceService {
1224
1970
  }
1225
1971
  let stdout: string;
1226
1972
  try {
1227
- ({ stdout } = await execFileAsync('git', ['-C', repoDir, 'status', '--porcelain=v1', '-z'], {
1228
- encoding: 'utf-8',
1229
- maxBuffer: 16 * 1024 * 1024,
1230
- }));
1973
+ ({ stdout } = await this.gitRunner.run(repoDir, ['status', '--porcelain=v1', '-z']));
1231
1974
  } catch (err) {
1232
- console.warn(
1233
- `[workspace] orphan scan via git status failed for ${workspaceId}:`,
1234
- err instanceof Error ? err.message : err,
1235
- );
1975
+ log.warn(`orphan scan via git status failed for ${workspaceId}:`, { err });
1236
1976
  return [];
1237
1977
  }
1238
1978
  if (!stdout.trim()) return [];
@@ -1301,10 +2041,7 @@ export class WorkspaceService implements IWorkspaceService {
1301
2041
  this.branchDirs.delete(branchForWorkspaceId(entry.name));
1302
2042
  removed.push(entry.name);
1303
2043
  } catch (err) {
1304
- console.warn(
1305
- `[workspace] sweep failed for ${entry.name}:`,
1306
- err instanceof Error ? err.message : err,
1307
- );
2044
+ log.warn(`sweep failed for ${entry.name}:`, { err });
1308
2045
  }
1309
2046
  }
1310
2047
  return { removed };
@@ -1370,7 +2107,7 @@ export class WorkspaceService implements IWorkspaceService {
1370
2107
  const workspaceDir = this.workspaceDirWithinRoot(workspaceId);
1371
2108
  try {
1372
2109
  await fs.access(path.join(workspaceDir, this.kbDirName, '.git'));
1373
- this.branchDirs.set(branch, workspaceDir);
2110
+ this.registerBranchDir(branch, workspaceDir);
1374
2111
  return workspaceDir;
1375
2112
  } catch {
1376
2113
  // Fall through to lazy bootstrap.
@@ -1394,76 +2131,118 @@ export class WorkspaceService implements IWorkspaceService {
1394
2131
  }
1395
2132
  }
1396
2133
 
1397
- private assertWithinWorkspace(absolutePath: string, workspaceDir: string): void {
1398
- const resolved = path.resolve(absolutePath);
1399
- const root = path.resolve(workspaceDir);
1400
- if (!resolved.startsWith(root + path.sep) && resolved !== root) {
1401
- throw new Error('Path traversal detected');
2134
+ private async buildFileTree(root: string, workspaceRoot: string, readFilter?: ReadTreeFilter): Promise<FileTreeEntry> {
2135
+ /** A folder while the tree is built: whether the caller may read it decides, once its subtree is known, whether it stays. */
2136
+ interface DirNode {
2137
+ name: string;
2138
+ relativePath: string;
2139
+ readable: boolean;
2140
+ /** Hidden by a listing decision, not a read rule: dropping it withholds nothing. */
2141
+ unlisted: boolean;
2142
+ children: (FileTreeEntry | DirNode)[];
2143
+ /**
2144
+ * Files directly in it the caller's read rules kept out — counted, never
2145
+ * named. Only an entry the filter judged unreadable counts:
2146
+ * `.bevelignore`d and `.git` entries never reach it, and an `'unlisted'`
2147
+ * one is kept from no one. Folders dropped below it are added in `finish`.
2148
+ */
2149
+ withheld: number;
1402
2150
  }
1403
- }
1404
-
1405
- private async buildFileTree(
1406
- dir: string,
1407
- workspaceRoot: string,
1408
- parentIgnore: BevelIgnoreStack,
1409
- readFilter?: ReadTreeFilter,
1410
- ): Promise<FileTreeEntry> {
1411
- const name = path.basename(dir);
1412
- const relativePath = path.relative(workspaceRoot, dir).replace(/\\/g, '/');
1413
- const entries = await fs.readdir(dir, { withFileTypes: true });
2151
+ const relOf = (abs: string) => path.relative(workspaceRoot, abs).replace(/\\/g, '/');
2152
+ const top: DirNode = { name: path.basename(root), relativePath: relOf(root) || '.', readable: true, unlisted: false, children: [], withheld: 0 };
2153
+ const nodes = new Map<string, DirNode>([['', top]]);
2154
+
2155
+ await this.disk.walk(root, explorerWalk(), [
2156
+ {
2157
+ async onDir(rel, entries) {
2158
+ const node = nodes.get(rel)!;
2159
+ const abs = path.join(root, rel);
2160
+ const rels = entries.map((e) => relOf(path.join(abs, e.name)));
2161
+ // Read-permission filter: ONE batched check per directory. Drops files the
2162
+ // caller can't read. A directory is kept when the caller can read it — a
2163
+ // readable directory left empty after filtering stays visible, the folder
2164
+ // itself is readable — OR when something readable survives beneath it: a
2165
+ // grant below (a linked skill's folder opened to a plugin's readers, a
2166
+ // sub-folder shared on its own) makes the folders above it the way there,
2167
+ // shown as containers whose own contents stay filtered. So an unreadable
2168
+ // directory is walked, not skipped, and dropped only when the walk finds
2169
+ // nothing (see `finish`). Fail-closed: anything not explicitly readable is
2170
+ // dropped. No filter → identical to the pre-feature tree (regression-safe).
2171
+ const verdict = readFilter && entries.length > 0 ? await readFilter(rels) : null;
2172
+ const readable = (rel: string) => verdict === null || verdict.get(rel) === true;
2173
+ const unlisted = (rel: string) => verdict !== null && verdict.get(rel) === 'unlisted';
2174
+ entries.forEach((entry, i) => {
2175
+ const entryRel = rels[i]!;
2176
+ if (entry.isDirectory()) {
2177
+ const child: DirNode = {
2178
+ name: entry.name,
2179
+ relativePath: entryRel,
2180
+ readable: readable(entryRel),
2181
+ unlisted: unlisted(entryRel),
2182
+ children: [],
2183
+ withheld: 0,
2184
+ };
2185
+ nodes.set(rel ? `${rel}/${entry.name}` : entry.name, child);
2186
+ node.children.push(child);
2187
+ } else if (readable(entryRel)) {
2188
+ node.children.push({ name: entry.name, relativePath: entryRel, type: 'file' });
2189
+ } else if (!unlisted(entryRel)) {
2190
+ node.withheld++;
2191
+ }
2192
+ });
2193
+ },
2194
+ },
2195
+ ]);
1414
2196
 
1415
- const ignoreStack = await parentIgnore.extendedWith(dir);
1416
-
1417
- // First pass: the entries surviving `.git`/`.gitkeep` + `.bevelignore`.
1418
- const candidates: { entry: (typeof entries)[number]; entryPath: string; rel: string }[] = [];
1419
- for (const entry of entries) {
1420
- // `.git/` is never user content and listing it would blow up the
1421
- // tree response. `.gitkeep` is the empty-folder placeholder — not
1422
- // worth surfacing in the file list.
1423
- if (entry.name === '.git' && entry.isDirectory()) continue;
1424
- if (entry.name === '.gitkeep' && entry.isFile()) continue;
1425
-
1426
- const entryPath = path.join(dir, entry.name);
1427
- if (ignoreStack.isIgnored(entryPath, entry.isDirectory())) continue;
1428
-
1429
- candidates.push({
1430
- entry,
1431
- entryPath,
1432
- rel: path.relative(workspaceRoot, entryPath).replace(/\\/g, '/'),
1433
- });
1434
- }
1435
-
1436
- // Read-permission filter: ONE batched check per directory. Drops files AND
1437
- // directories the caller can't read; a hidden directory's subtree is never
1438
- // walked (skip-and-don't-recurse). A readable directory left empty after
1439
- // filtering stays visible (the folder itself is readable). Fail-closed:
1440
- // anything not explicitly readable is dropped. No filter → identical to the
1441
- // pre-feature tree (regression-safe).
1442
- let visible = candidates;
1443
- if (readFilter && candidates.length > 0) {
1444
- const verdict = await readFilter(candidates.map((c) => c.rel));
1445
- visible = candidates.filter((c) => verdict.get(c.rel) === true);
1446
- }
1447
-
1448
- const children: FileTreeEntry[] = [];
1449
- for (const { entry, entryPath, rel } of visible) {
1450
- if (entry.isDirectory()) {
1451
- children.push(await this.buildFileTree(entryPath, workspaceRoot, ignoreStack, readFilter));
1452
- } else {
1453
- children.push({ name: entry.name, relativePath: rel, type: 'file' });
2197
+ /**
2198
+ * Bottom-up: drop a folder the caller may not read once nothing readable
2199
+ * turned up beneath it; sort each level. Each folder that stays carries
2200
+ * how many entries were withheld anywhere under it (a dropped folder
2201
+ * counts as one, its own subtree's count included), so a tree drawn from
2202
+ * ONE folder — the Library's, a delete's — can tell "empty" from "kept
2203
+ * from you" for that folder rather than for the listing. Only when
2204
+ * something was withheld, so an unfiltered — or all-readable — listing
2205
+ * stays exactly the tree it always was.
2206
+ */
2207
+ const finish = (node: DirNode): FileTreeEntry => {
2208
+ const children: FileTreeEntry[] = [];
2209
+ let withheld = node.withheld;
2210
+ for (const child of node.children) {
2211
+ if ('type' in child) {
2212
+ children.push(child);
2213
+ continue;
2214
+ }
2215
+ const sub = finish(child);
2216
+ withheld += sub.withheld ?? 0;
2217
+ if (child.readable || (sub.children?.length ?? 0) > 0) children.push(sub);
2218
+ else if (!child.unlisted) withheld++;
1454
2219
  }
1455
- }
1456
-
1457
- children.sort((a, b) => {
1458
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
1459
- return a.name.localeCompare(b.name);
1460
- });
1461
-
1462
- return {
1463
- name,
1464
- relativePath: relativePath || '.',
1465
- type: 'directory',
1466
- children,
2220
+ children.sort((a, b) => {
2221
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
2222
+ return a.name.localeCompare(b.name);
2223
+ });
2224
+ const entry: FileTreeEntry = { name: node.name, relativePath: node.relativePath, type: 'directory', children };
2225
+ return withheld > 0 ? { ...entry, withheld } : entry;
1467
2226
  };
2227
+ return finish(top);
1468
2228
  }
1469
2229
  }
2230
+
2231
+ /**
2232
+ * The explorer's walk of a workspace: no git internals (never user content,
2233
+ * and listing it would blow up the tree), no `.gitkeep` (the empty-folder
2234
+ * placeholder, not worth showing), `.bevelignore` honoured on the way down —
2235
+ * and a folder that cannot be listed is the request's error, not a tree with
2236
+ * a silent gap. A folder download and the whole-KB read walk the same way,
2237
+ * so they carry exactly what the explorer shows.
2238
+ */
2239
+ function explorerWalk(): TreeWalkOptions {
2240
+ return {
2241
+ // Any spelling of the git folder (`.GIT`, `.git.`) — the same rule the path
2242
+ // guards apply, so a download or listing never carries what they refuse —
2243
+ // and the empty-folder placeholder, which is never content.
2244
+ skip: (e) => hasGitInternalsSegment(e.name) || (isFolderPlaceholder(e.name) && e.isFile()),
2245
+ ignore: true,
2246
+ unreadable: 'throw',
2247
+ };
2248
+ }