@bevel-software/platform-core-backend 0.15.1 → 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 (1220) hide show
  1. package/THIRD-PARTY-NOTICES.md +4 -4
  2. package/dist/core/catalog-cache-invalidation.d.ts +16 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
  4. package/dist/core/catalog-cache-invalidation.js +6 -1
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -1
  6. package/dist/core/catalog-revision.d.ts +39 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -0
  8. package/dist/core/catalog-revision.js +94 -0
  9. package/dist/core/catalog-revision.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +7 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +199 -75
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +49 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +212 -86
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +138 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -0
  20. package/dist/core/lifecycle.js +218 -0
  21. package/dist/core/lifecycle.js.map +1 -0
  22. package/dist/core/public-config.d.ts +82 -0
  23. package/dist/core/public-config.d.ts.map +1 -0
  24. package/dist/core/public-config.js +78 -0
  25. package/dist/core/public-config.js.map +1 -0
  26. package/dist/core/readiness.d.ts +86 -0
  27. package/dist/core/readiness.d.ts.map +1 -0
  28. package/dist/core/readiness.js +56 -0
  29. package/dist/core/readiness.js.map +1 -0
  30. package/dist/core-config.d.ts +30 -0
  31. package/dist/core-config.d.ts.map +1 -1
  32. package/dist/core-config.js +64 -2
  33. package/dist/core-config.js.map +1 -1
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/access/access-control.interface.d.ts +215 -11
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +57 -11
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +616 -155
  43. package/dist/modules/access/access-control.service.js.map +1 -1
  44. package/dist/modules/access/access-mutation.service.d.ts +71 -5
  45. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-mutation.service.js +161 -13
  47. package/dist/modules/access/access-mutation.service.js.map +1 -1
  48. package/dist/modules/access/access-view.d.ts +147 -0
  49. package/dist/modules/access/access-view.d.ts.map +1 -0
  50. package/dist/modules/access/access-view.js +211 -0
  51. package/dist/modules/access/access-view.js.map +1 -0
  52. package/dist/modules/access/access.routes.d.ts.map +1 -1
  53. package/dist/modules/access/access.routes.js +255 -79
  54. package/dist/modules/access/access.routes.js.map +1 -1
  55. package/dist/modules/access/admin-locked-commit.d.ts +2 -3
  56. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  57. package/dist/modules/access/admin-locked-commit.js +9 -8
  58. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  59. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
  60. package/dist/modules/access/admin-route-helpers.js +4 -4
  61. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  62. package/dist/modules/access/change-read-gate.d.ts +53 -0
  63. package/dist/modules/access/change-read-gate.d.ts.map +1 -0
  64. package/dist/modules/access/change-read-gate.js +134 -0
  65. package/dist/modules/access/change-read-gate.js.map +1 -0
  66. package/dist/modules/access/creator-access.d.ts +33 -39
  67. package/dist/modules/access/creator-access.d.ts.map +1 -1
  68. package/dist/modules/access/creator-access.js +92 -119
  69. package/dist/modules/access/creator-access.js.map +1 -1
  70. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  71. package/dist/modules/access/directory-sync-bot.js +3 -1
  72. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  73. package/dist/modules/access/groups-admin.service.d.ts +3 -3
  74. package/dist/modules/access/groups-admin.service.js +2 -2
  75. package/dist/modules/access/reference-scan.d.ts +1 -1
  76. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  77. package/dist/modules/access/reference-scan.js +5 -4
  78. package/dist/modules/access/reference-scan.js.map +1 -1
  79. package/dist/modules/access/roles-admin.service.d.ts +72 -13
  80. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  81. package/dist/modules/access/roles-admin.service.js +71 -13
  82. package/dist/modules/access/roles-admin.service.js.map +1 -1
  83. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  84. package/dist/modules/access/synced-groups-committer.js +8 -6
  85. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
  87. package/dist/modules/access/synced-groups-writer.js +3 -8
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  89. package/dist/modules/access/user-access-removal.service.d.ts +209 -0
  90. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
  91. package/dist/modules/access/user-access-removal.service.js +662 -0
  92. package/dist/modules/access/user-access-removal.service.js.map +1 -0
  93. package/dist/modules/access-model/access-errors.d.ts +32 -1
  94. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  95. package/dist/modules/access-model/access-errors.js +38 -11
  96. package/dist/modules/access-model/access-errors.js.map +1 -1
  97. package/dist/modules/access-model/access-grammar.d.ts +49 -24
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +68 -48
  100. package/dist/modules/access-model/access-grammar.js.map +1 -1
  101. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  102. package/dist/modules/access-model/access-splice.js +18 -18
  103. package/dist/modules/access-model/access-splice.js.map +1 -1
  104. package/dist/modules/access-model/access-template.d.ts +22 -0
  105. package/dist/modules/access-model/access-template.d.ts.map +1 -0
  106. package/dist/modules/access-model/access-template.js +42 -0
  107. package/dist/modules/access-model/access-template.js.map +1 -0
  108. package/dist/modules/access-model/change-gate.d.ts +65 -0
  109. package/dist/modules/access-model/change-gate.d.ts.map +1 -0
  110. package/dist/modules/access-model/change-gate.js +25 -0
  111. package/dist/modules/access-model/change-gate.js.map +1 -0
  112. package/dist/modules/access-model/creator.d.ts +13 -16
  113. package/dist/modules/access-model/creator.d.ts.map +1 -1
  114. package/dist/modules/access-model/creator.js.map +1 -1
  115. package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
  116. package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
  117. package/dist/modules/access-model/frontmatter-lines.js +74 -0
  118. package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
  119. package/dist/modules/access-model/plugin-principals.d.ts +5 -3
  120. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -1
  121. package/dist/modules/access-model/plugin-principals.js +11 -17
  122. package/dist/modules/access-model/plugin-principals.js.map +1 -1
  123. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  124. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  125. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  126. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  127. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  128. package/dist/modules/admin/admin-access.service.js +5 -2
  129. package/dist/modules/admin/admin-access.service.js.map +1 -1
  130. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  131. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  132. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  133. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  134. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  135. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  136. package/dist/modules/agent-instructions/compose.js +234 -0
  137. package/dist/modules/agent-instructions/compose.js.map +1 -0
  138. package/dist/modules/agent-instructions/index.d.ts +4 -0
  139. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  140. package/dist/modules/agent-instructions/index.js +4 -0
  141. package/dist/modules/agent-instructions/index.js.map +1 -0
  142. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  143. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  144. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  145. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  146. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  147. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  148. package/dist/modules/auth/account-erasure.service.js +96 -10
  149. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  150. package/dist/modules/auth/account.routes.d.ts +6 -3
  151. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  152. package/dist/modules/auth/account.routes.js +109 -10
  153. package/dist/modules/auth/account.routes.js.map +1 -1
  154. package/dist/modules/auth/auth.routes.d.ts +10 -3
  155. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  156. package/dist/modules/auth/auth.routes.js +9 -4
  157. package/dist/modules/auth/auth.routes.js.map +1 -1
  158. package/dist/modules/auth/auth.service.d.ts +89 -5
  159. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  160. package/dist/modules/auth/auth.service.js +160 -25
  161. package/dist/modules/auth/auth.service.js.map +1 -1
  162. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  163. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  164. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  165. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  166. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  167. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  168. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  169. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  170. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  171. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  172. package/dist/modules/database/advisory-lock.d.ts +103 -0
  173. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  174. package/dist/modules/database/advisory-lock.js +320 -0
  175. package/dist/modules/database/advisory-lock.js.map +1 -0
  176. package/dist/modules/database/connection.d.ts.map +1 -1
  177. package/dist/modules/database/connection.js +9 -1
  178. package/dist/modules/database/connection.js.map +1 -1
  179. package/dist/modules/database/core-schema.d.ts +346 -0
  180. package/dist/modules/database/core-schema.d.ts.map +1 -1
  181. package/dist/modules/database/core-schema.js +119 -0
  182. package/dist/modules/database/core-schema.js.map +1 -1
  183. package/dist/modules/database/migrate.d.ts.map +1 -1
  184. package/dist/modules/database/migrate.js +26 -6
  185. package/dist/modules/database/migrate.js.map +1 -1
  186. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  187. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  188. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  189. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  190. package/dist/modules/diff/diff.routes.js +2 -4
  191. package/dist/modules/diff/diff.routes.js.map +1 -1
  192. package/dist/modules/diff/diff.service.d.ts +4 -1
  193. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  194. package/dist/modules/diff/diff.service.js +79 -81
  195. package/dist/modules/diff/diff.service.js.map +1 -1
  196. package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
  197. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  198. package/dist/modules/kb-fs/bevel-ignore.js +71 -0
  199. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  200. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  201. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  202. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  203. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
  204. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  205. package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
  206. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  207. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  208. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  209. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  210. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  211. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  212. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  213. package/dist/modules/kb-fs/node-fs.js +158 -0
  214. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  215. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  216. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  217. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  218. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  219. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  220. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  221. package/dist/modules/kb-fs/repo-path.js +195 -3
  222. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  223. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  224. package/dist/modules/kb-sync/kb-sync.routes.js +4 -2
  225. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  226. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  227. package/dist/modules/kb-sync/kb-sync.service.js +7 -5
  228. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  229. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -1
  230. package/dist/modules/marketplace/git-http.routes.js +6 -4
  231. package/dist/modules/marketplace/git-http.routes.js.map +1 -1
  232. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +12 -2
  233. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -1
  234. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +32 -3
  235. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -1
  236. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +27 -0
  237. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -1
  238. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +45 -0
  239. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -1
  240. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -1
  241. package/dist/modules/marketplace/github-facade/github-facade.routes.js +30 -6
  242. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -1
  243. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +21 -9
  244. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -1
  245. package/dist/modules/marketplace/github-facade/github-facade.service.js +60 -19
  246. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -1
  247. package/dist/modules/marketplace/github-facade/index.d.ts +1 -1
  248. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -1
  249. package/dist/modules/marketplace/github-facade/index.js +1 -1
  250. package/dist/modules/marketplace/github-facade/index.js.map +1 -1
  251. package/dist/modules/marketplace/marketplace-repo.service.d.ts +14 -1
  252. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -1
  253. package/dist/modules/marketplace/marketplace-repo.service.js +26 -8
  254. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -1
  255. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  256. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  257. package/dist/modules/mcp/downstream-pool.js +221 -0
  258. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  259. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  260. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  261. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  262. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  263. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  264. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  265. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  266. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  267. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  268. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  269. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  270. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  271. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  272. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  273. package/dist/modules/mcp/first-call-probe.js +298 -0
  274. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  275. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  276. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  277. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  278. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  279. package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
  280. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  281. package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
  282. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  283. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  284. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  285. package/dist/modules/mcp/mcp.routes.js +83 -179
  286. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  287. package/dist/modules/mcp/mcp.service.d.ts +218 -61
  288. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  289. package/dist/modules/mcp/mcp.service.js +713 -159
  290. package/dist/modules/mcp/mcp.service.js.map +1 -1
  291. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  292. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -2
  293. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  294. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  295. package/dist/modules/mcp/oauth/oauth-consent.routes.js +3 -1
  296. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  297. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  298. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  299. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  300. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  301. package/dist/modules/plugins/compile/compile-marketplace.d.ts +13 -6
  302. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -1
  303. package/dist/modules/plugins/compile/compile-marketplace.js +119 -36
  304. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -1
  305. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +8 -1
  306. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -1
  307. package/dist/modules/plugins/compile/marketplace-compiler.service.js +9 -6
  308. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -1
  309. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -1
  310. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +32 -9
  311. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -1
  312. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +26 -6
  313. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -1
  314. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +71 -14
  315. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -1
  316. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +4 -2
  317. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -1
  318. package/dist/modules/plugins/discovery/kb-plugin-source.js +9 -6
  319. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -1
  320. package/dist/modules/plugins/discovery/native.source.d.ts +2 -1
  321. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -1
  322. package/dist/modules/plugins/discovery/native.source.js +22 -4
  323. package/dist/modules/plugins/discovery/native.source.js.map +1 -1
  324. package/dist/modules/plugins/discovery/plugin-source.d.ts +17 -3
  325. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -1
  326. package/dist/modules/plugins/index.d.ts +6 -3
  327. package/dist/modules/plugins/index.d.ts.map +1 -1
  328. package/dist/modules/plugins/index.js +6 -3
  329. package/dist/modules/plugins/index.js.map +1 -1
  330. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  331. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  332. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  333. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  334. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  335. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  336. package/dist/modules/plugins/join-request-records.store.js +179 -0
  337. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  338. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  339. package/dist/modules/plugins/join-requests.service.js +4 -2
  340. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  341. package/dist/modules/plugins/plugin-links.d.ts +4 -1
  342. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  343. package/dist/modules/plugins/plugin-links.js +6 -3
  344. package/dist/modules/plugins/plugin-links.js.map +1 -1
  345. package/dist/modules/plugins/plugin-links.service.d.ts +2 -5
  346. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -1
  347. package/dist/modules/plugins/plugin-links.service.js +4 -13
  348. package/dist/modules/plugins/plugin-links.service.js.map +1 -1
  349. package/dist/modules/plugins/plugin-provision.service.d.ts +91 -19
  350. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  351. package/dist/modules/plugins/plugin-provision.service.js +183 -38
  352. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  353. package/dist/modules/plugins/plugin-rename.service.d.ts +3 -1
  354. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -1
  355. package/dist/modules/plugins/plugin-rename.service.js +20 -11
  356. package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
  357. package/dist/modules/plugins/plugins.contract.d.ts +52 -4
  358. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  359. package/dist/modules/plugins/plugins.routes.d.ts +38 -5
  360. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  361. package/dist/modules/plugins/plugins.routes.js +190 -116
  362. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  363. package/dist/modules/plugins/plugins.service.d.ts +38 -6
  364. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  365. package/dist/modules/plugins/plugins.service.js +86 -17
  366. package/dist/modules/plugins/plugins.service.js.map +1 -1
  367. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  368. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  369. package/dist/modules/plugins/plugins.tools.js +82 -0
  370. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  371. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  372. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  373. package/dist/modules/plugins/teams.routes.js +79 -0
  374. package/dist/modules/plugins/teams.routes.js.map +1 -0
  375. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  376. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  377. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  378. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  379. package/dist/modules/secrets-vault/index.d.ts +1 -1
  380. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  381. package/dist/modules/secrets-vault/index.js.map +1 -1
  382. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  383. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  384. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  385. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  386. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  387. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  388. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  389. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  390. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  391. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  392. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  393. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  394. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  395. package/dist/modules/settings/connection-check.d.ts +118 -0
  396. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  397. package/dist/modules/settings/connection-check.js +287 -0
  398. package/dist/modules/settings/connection-check.js.map +1 -0
  399. package/dist/modules/settings/deployment-settings.service.d.ts +153 -7
  400. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  401. package/dist/modules/settings/deployment-settings.service.js +348 -51
  402. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  403. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  404. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  405. package/dist/modules/settings/git-root-folders.js +130 -0
  406. package/dist/modules/settings/git-root-folders.js.map +1 -0
  407. package/dist/modules/settings/oidc-check.d.ts +85 -0
  408. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  409. package/dist/modules/settings/oidc-check.js +172 -0
  410. package/dist/modules/settings/oidc-check.js.map +1 -0
  411. package/dist/modules/settings/setup.routes.d.ts +31 -1
  412. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  413. package/dist/modules/settings/setup.routes.js +400 -108
  414. package/dist/modules/settings/setup.routes.js.map +1 -1
  415. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  416. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  417. package/dist/modules/skills/allowed-tools-check.js +260 -0
  418. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  419. package/dist/modules/skills/index.d.ts +3 -1
  420. package/dist/modules/skills/index.d.ts.map +1 -1
  421. package/dist/modules/skills/index.js +1 -0
  422. package/dist/modules/skills/index.js.map +1 -1
  423. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  424. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  425. package/dist/modules/skills/pending-skills.service.js +18 -70
  426. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  427. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  428. package/dist/modules/skills/skill-access-requests.routes.js +9 -6
  429. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  430. package/dist/modules/skills/skills.contract.d.ts +44 -10
  431. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  432. package/dist/modules/skills/skills.routes.d.ts +8 -1
  433. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  434. package/dist/modules/skills/skills.routes.js +16 -2
  435. package/dist/modules/skills/skills.routes.js.map +1 -1
  436. package/dist/modules/skills/skills.service.d.ts +31 -7
  437. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  438. package/dist/modules/skills/skills.service.js +191 -81
  439. package/dist/modules/skills/skills.service.js.map +1 -1
  440. package/dist/modules/skills/skills.tools.d.ts +4 -1
  441. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  442. package/dist/modules/skills/skills.tools.js +41 -6
  443. package/dist/modules/skills/skills.tools.js.map +1 -1
  444. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  445. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  446. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  447. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  448. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  449. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  450. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  451. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  452. package/dist/modules/tool-auth/external-api-key.interface.d.ts +51 -3
  453. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  454. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  455. package/dist/modules/tool-auth/external-api-key.service.d.ts +16 -6
  456. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  457. package/dist/modules/tool-auth/external-api-key.service.js +61 -18
  458. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  459. package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
  460. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  461. package/dist/modules/tool-auth/internal-token.service.js +5 -2
  462. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  463. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  464. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  465. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  466. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  467. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
  468. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  469. package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
  470. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  471. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  472. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  473. package/dist/modules/tool-helpers/tool-context.js +28 -6
  474. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  475. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  476. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  477. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  478. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  479. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  480. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  481. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  482. package/dist/modules/tool-manuals/index.d.ts +2 -1
  483. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  484. package/dist/modules/tool-manuals/index.js +1 -0
  485. package/dist/modules/tool-manuals/index.js.map +1 -1
  486. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  487. package/dist/modules/tool-manuals/mcp-json-discovery.js +8 -6
  488. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  489. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  490. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  491. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  492. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  493. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  494. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  495. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  496. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  497. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  498. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  499. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  500. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  501. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  502. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  503. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  504. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  505. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  506. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  507. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  508. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  509. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +119 -5
  510. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  511. package/dist/modules/tool-manuals/tool-manuals.service.js +404 -74
  512. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  513. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  514. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  515. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  516. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  517. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  518. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  519. package/dist/modules/workflow/event-bus.d.ts +10 -4
  520. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  521. package/dist/modules/workflow/event-bus.js +14 -11
  522. package/dist/modules/workflow/event-bus.js.map +1 -1
  523. package/dist/modules/workflow/events.routes.d.ts +17 -2
  524. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  525. package/dist/modules/workflow/events.routes.js +31 -12
  526. package/dist/modules/workflow/events.routes.js.map +1 -1
  527. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  528. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  529. package/dist/modules/workflow/file-lock.service.js +27 -4
  530. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  531. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  532. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  533. package/dist/modules/workflow/git/change-request-link.js +34 -0
  534. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  535. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  536. package/dist/modules/workflow/git/git-version.js +3 -1
  537. package/dist/modules/workflow/git/git-version.js.map +1 -1
  538. package/dist/modules/workflow/git/git.service.d.ts +263 -4
  539. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  540. package/dist/modules/workflow/git/git.service.js +528 -118
  541. package/dist/modules/workflow/git/git.service.js.map +1 -1
  542. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  543. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  544. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  545. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  546. package/dist/modules/workflow/git/pull-request.service.d.ts +51 -2
  547. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  548. package/dist/modules/workflow/git/pull-request.service.js +157 -25
  549. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  550. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  551. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  552. package/dist/modules/workflow/pending-commits.service.js +24 -4
  553. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  554. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  555. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  556. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  557. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  558. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  559. package/dist/modules/workflow/recovery-bot.js +3 -1
  560. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  561. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  562. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  563. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  564. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  565. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  566. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  567. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  568. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  569. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  570. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  571. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  572. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  573. package/dist/modules/workflow/sanitize-error.js +8 -4
  574. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  575. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  576. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  577. package/dist/modules/workflow/workflow.routes.js +259 -29
  578. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  579. package/dist/modules/workflow/workflow.service.d.ts +166 -14
  580. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  581. package/dist/modules/workflow/workflow.service.js +906 -106
  582. package/dist/modules/workflow/workflow.service.js.map +1 -1
  583. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  584. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  585. package/dist/modules/workspace/empty-dirs.js +90 -0
  586. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  587. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  588. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  589. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  590. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  591. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  592. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  593. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  594. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  595. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  596. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  597. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  598. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  599. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  600. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  601. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  602. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  603. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  604. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  605. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  606. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  607. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  608. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  609. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  610. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  611. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  612. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  613. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  614. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  615. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  616. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  617. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  618. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  619. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  620. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  621. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  622. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  623. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  624. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  625. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  626. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  627. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  628. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  629. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  630. package/dist/modules/workspace/not-found.d.ts +80 -0
  631. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  632. package/dist/modules/workspace/not-found.js +112 -0
  633. package/dist/modules/workspace/not-found.js.map +1 -0
  634. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  635. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  636. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  637. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  638. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  639. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  640. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  641. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  642. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  643. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  644. package/dist/modules/workspace/startup/kb-git.js +34 -35
  645. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  646. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  647. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  648. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  649. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  650. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  651. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  652. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  653. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  654. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  655. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  656. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  657. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  658. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  659. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  660. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  661. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  662. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  663. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  664. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  665. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  666. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +7 -1
  667. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -1
  668. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +45 -81
  669. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -1
  670. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  671. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  672. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  673. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  674. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  675. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  676. package/dist/modules/workspace/startup/steps/seed-tree.js +154 -78
  677. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  678. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  679. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  680. package/dist/modules/workspace/startup/steps/template-files.step.js +446 -138
  681. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  682. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  683. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  684. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  685. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  686. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  687. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  688. package/dist/modules/workspace/workspace.routes.js +649 -238
  689. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  690. package/dist/modules/workspace/workspace.service.d.ts +290 -12
  691. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  692. package/dist/modules/workspace/workspace.service.js +924 -269
  693. package/dist/modules/workspace/workspace.service.js.map +1 -1
  694. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  695. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  696. package/dist/modules/workspace/workspace.tools.js +1590 -146
  697. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  698. package/dist/modules/workspace/write-denial.d.ts +76 -0
  699. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  700. package/dist/modules/workspace/write-denial.js +138 -0
  701. package/dist/modules/workspace/write-denial.js.map +1 -0
  702. package/dist/shared/canonical-file-identity.d.ts +64 -0
  703. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  704. package/dist/shared/canonical-file-identity.js +92 -0
  705. package/dist/shared/canonical-file-identity.js.map +1 -0
  706. package/dist/shared/domain-errors.d.ts +108 -0
  707. package/dist/shared/domain-errors.d.ts.map +1 -1
  708. package/dist/shared/domain-errors.js +137 -0
  709. package/dist/shared/domain-errors.js.map +1 -1
  710. package/dist/shared/email-identity.d.ts +32 -0
  711. package/dist/shared/email-identity.d.ts.map +1 -0
  712. package/dist/shared/email-identity.js +37 -0
  713. package/dist/shared/email-identity.js.map +1 -0
  714. package/dist/shared/fs.contract.d.ts +206 -0
  715. package/dist/shared/fs.contract.d.ts.map +1 -0
  716. package/dist/shared/fs.contract.js +79 -0
  717. package/dist/shared/fs.contract.js.map +1 -0
  718. package/dist/shared/git-failure.d.ts +80 -0
  719. package/dist/shared/git-failure.d.ts.map +1 -0
  720. package/dist/shared/git-failure.js +139 -0
  721. package/dist/shared/git-failure.js.map +1 -0
  722. package/dist/shared/git-internals.d.ts +17 -0
  723. package/dist/shared/git-internals.d.ts.map +1 -0
  724. package/dist/shared/git-internals.js +123 -0
  725. package/dist/shared/git-internals.js.map +1 -0
  726. package/dist/shared/git.contract.d.ts +157 -0
  727. package/dist/shared/git.contract.d.ts.map +1 -0
  728. package/dist/shared/git.contract.js +103 -0
  729. package/dist/shared/git.contract.js.map +1 -0
  730. package/dist/shared/http-errors.d.ts +33 -0
  731. package/dist/shared/http-errors.d.ts.map +1 -0
  732. package/dist/shared/http-errors.js +35 -0
  733. package/dist/shared/http-errors.js.map +1 -0
  734. package/dist/shared/logger.contract.d.ts +63 -0
  735. package/dist/shared/logger.contract.d.ts.map +1 -0
  736. package/dist/shared/logger.contract.js +126 -0
  737. package/dist/shared/logger.contract.js.map +1 -0
  738. package/dist/shared/logging.d.ts +31 -0
  739. package/dist/shared/logging.d.ts.map +1 -0
  740. package/dist/shared/logging.js +92 -0
  741. package/dist/shared/logging.js.map +1 -0
  742. package/dist/shared/path-containment.d.ts +21 -0
  743. package/dist/shared/path-containment.d.ts.map +1 -0
  744. package/dist/shared/path-containment.js +34 -0
  745. package/dist/shared/path-containment.js.map +1 -0
  746. package/dist/shared/pending-proposals.d.ts +71 -0
  747. package/dist/shared/pending-proposals.d.ts.map +1 -0
  748. package/dist/shared/pending-proposals.js +98 -0
  749. package/dist/shared/pending-proposals.js.map +1 -0
  750. package/dist/shared/printable.d.ts +22 -0
  751. package/dist/shared/printable.d.ts.map +1 -0
  752. package/dist/shared/printable.js +65 -0
  753. package/dist/shared/printable.js.map +1 -0
  754. package/dist/shared/redact-secret.d.ts +30 -0
  755. package/dist/shared/redact-secret.d.ts.map +1 -0
  756. package/dist/shared/redact-secret.js +114 -0
  757. package/dist/shared/redact-secret.js.map +1 -0
  758. package/dist/shared/rename-no-replace.d.ts +119 -0
  759. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  760. package/dist/shared/rename-no-replace.js +323 -0
  761. package/dist/shared/rename-no-replace.js.map +1 -0
  762. package/dist/shared/ssrf.d.ts.map +1 -1
  763. package/dist/shared/ssrf.js +49 -18
  764. package/dist/shared/ssrf.js.map +1 -1
  765. package/dist/version.d.ts +10 -6
  766. package/dist/version.d.ts.map +1 -1
  767. package/dist/version.js +10 -6
  768. package/dist/version.js.map +1 -1
  769. package/kb-template/.bevelignore +4 -3
  770. package/kb-template/AGENTS.md +281 -36
  771. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  772. package/kb-template/access.md +5 -3
  773. package/kb-template/mcp-description.md +27 -0
  774. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  775. package/migrations/0007_github_facade_registered.sql +1 -0
  776. package/migrations/0008_change_request_apply_failure.sql +4 -0
  777. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  778. package/migrations/0010_plugin_join_requests.sql +15 -0
  779. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  780. package/migrations/meta/0006_snapshot.json +1723 -0
  781. package/migrations/meta/0007_snapshot.json +1729 -0
  782. package/migrations/meta/0008_snapshot.json +1753 -0
  783. package/migrations/meta/0009_snapshot.json +1759 -0
  784. package/migrations/meta/0010_snapshot.json +1872 -0
  785. package/migrations/meta/0011_snapshot.json +1884 -0
  786. package/migrations/meta/_journal.json +42 -0
  787. package/package.json +6 -5
  788. package/src/__tests__/core-config.domain.test.ts +67 -2
  789. package/src/__tests__/kb-layout-config.test.ts +186 -1
  790. package/src/__tests__/open-change-gate.ts +15 -0
  791. package/src/core/__tests__/catalog-cache-invalidation.test.ts +83 -3
  792. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  793. package/src/core/__tests__/lifecycle.test.ts +369 -0
  794. package/src/core/__tests__/public-config.test.ts +30 -0
  795. package/src/core/__tests__/readiness.test.ts +109 -0
  796. package/src/core/catalog-cache-invalidation.ts +20 -1
  797. package/src/core/catalog-revision.ts +110 -0
  798. package/src/core/create-core-server.ts +258 -79
  799. package/src/core/create-core-services.ts +302 -81
  800. package/src/core/lifecycle.ts +312 -0
  801. package/src/core/public-config.ts +86 -0
  802. package/src/core/readiness.ts +123 -0
  803. package/src/core-config.ts +70 -4
  804. package/src/index.ts +15 -0
  805. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  806. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  807. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  808. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  809. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  810. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  811. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  812. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +2 -0
  813. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  814. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  815. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  816. package/src/modules/access/__tests__/access-control.service.test.ts +123 -78
  817. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  818. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  819. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  820. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  821. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  822. package/src/modules/access/__tests__/access-plugin-principals.test.ts +18 -1
  823. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  824. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  825. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  826. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  827. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  828. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  829. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  830. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  831. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  832. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  833. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  834. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  835. package/src/modules/access/__tests__/groups-admin.service.test.ts +6 -5
  836. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  837. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  838. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  839. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  840. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  841. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  842. package/src/modules/access/access-control.interface.ts +251 -11
  843. package/src/modules/access/access-control.service.ts +719 -164
  844. package/src/modules/access/access-mutation.service.ts +182 -13
  845. package/src/modules/access/access-view.ts +316 -0
  846. package/src/modules/access/access.routes.ts +293 -95
  847. package/src/modules/access/admin-locked-commit.ts +12 -14
  848. package/src/modules/access/admin-route-helpers.ts +4 -4
  849. package/src/modules/access/change-read-gate.ts +148 -0
  850. package/src/modules/access/creator-access.ts +89 -124
  851. package/src/modules/access/directory-sync-bot.ts +4 -1
  852. package/src/modules/access/groups-admin.service.ts +3 -3
  853. package/src/modules/access/reference-scan.ts +5 -5
  854. package/src/modules/access/roles-admin.service.ts +94 -15
  855. package/src/modules/access/synced-groups-committer.ts +10 -15
  856. package/src/modules/access/synced-groups-writer.ts +3 -12
  857. package/src/modules/access/user-access-removal.service.ts +711 -0
  858. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  859. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  860. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  861. package/src/modules/access-model/access-errors.ts +56 -15
  862. package/src/modules/access-model/access-grammar.ts +85 -48
  863. package/src/modules/access-model/access-splice.ts +18 -18
  864. package/src/modules/access-model/access-template.ts +44 -0
  865. package/src/modules/access-model/change-gate.ts +80 -0
  866. package/src/modules/access-model/creator.ts +18 -21
  867. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  868. package/src/modules/access-model/plugin-principals.ts +12 -19
  869. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  870. package/src/modules/admin/admin-access.service.ts +6 -2
  871. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  872. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  873. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  874. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  875. package/src/modules/agent-instructions/compose.ts +271 -0
  876. package/src/modules/agent-instructions/index.ts +14 -0
  877. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  878. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  879. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  880. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  881. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  882. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  883. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  884. package/src/modules/auth/account-erasure.service.ts +117 -10
  885. package/src/modules/auth/account.routes.ts +115 -14
  886. package/src/modules/auth/auth.routes.ts +20 -7
  887. package/src/modules/auth/auth.service.ts +187 -28
  888. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  889. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  890. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  891. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  892. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  893. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  894. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  895. package/src/modules/database/advisory-lock.ts +346 -0
  896. package/src/modules/database/connection.ts +9 -1
  897. package/src/modules/database/core-schema.ts +123 -0
  898. package/src/modules/database/migrate.ts +27 -6
  899. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  900. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  901. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  902. package/src/modules/diff/diff.routes.ts +2 -4
  903. package/src/modules/diff/diff.service.ts +75 -79
  904. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  905. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  906. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  907. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  908. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  909. package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
  910. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  911. package/src/modules/kb-fs/node-fs.ts +165 -0
  912. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  913. package/src/modules/kb-fs/repo-path.ts +213 -3
  914. package/src/modules/kb-sync/kb-sync.routes.ts +5 -2
  915. package/src/modules/kb-sync/kb-sync.service.ts +8 -5
  916. package/src/modules/marketplace/__tests__/github-facade.test.ts +134 -2
  917. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +2 -1
  918. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  919. package/src/modules/marketplace/git-http.routes.ts +7 -4
  920. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +35 -3
  921. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +55 -0
  922. package/src/modules/marketplace/github-facade/github-facade.routes.ts +32 -6
  923. package/src/modules/marketplace/github-facade/github-facade.service.ts +80 -19
  924. package/src/modules/marketplace/github-facade/index.ts +1 -0
  925. package/src/modules/marketplace/marketplace-repo.service.ts +23 -8
  926. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  927. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  928. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  929. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  930. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  931. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  932. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
  933. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  934. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  935. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  936. package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
  937. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  938. package/src/modules/mcp/downstream-pool.ts +256 -0
  939. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  940. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  941. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  942. package/src/modules/mcp/first-call-probe.ts +418 -0
  943. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  944. package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
  945. package/src/modules/mcp/mcp.routes.ts +376 -482
  946. package/src/modules/mcp/mcp.service.ts +846 -176
  947. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +5 -2
  948. package/src/modules/mcp/oauth/oauth-consent.routes.ts +4 -4
  949. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  950. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +10 -7
  951. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  952. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +139 -10
  953. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  954. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  955. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  956. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +148 -7
  957. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +10 -13
  958. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  959. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +129 -19
  960. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +91 -29
  961. package/src/modules/plugins/__tests__/plugins.routes.test.ts +370 -19
  962. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  963. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  964. package/src/modules/plugins/compile/compile-marketplace.ts +143 -46
  965. package/src/modules/plugins/compile/marketplace-compiler.service.ts +8 -7
  966. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +56 -20
  967. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +30 -9
  968. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +93 -18
  969. package/src/modules/plugins/discovery/kb-plugin-source.ts +10 -7
  970. package/src/modules/plugins/discovery/native.source.ts +21 -3
  971. package/src/modules/plugins/discovery/plugin-source.ts +17 -3
  972. package/src/modules/plugins/index.ts +16 -3
  973. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  974. package/src/modules/plugins/join-request-records.store.ts +353 -0
  975. package/src/modules/plugins/join-requests.service.ts +7 -4
  976. package/src/modules/plugins/plugin-links.service.ts +4 -17
  977. package/src/modules/plugins/plugin-links.ts +6 -3
  978. package/src/modules/plugins/plugin-provision.service.ts +209 -37
  979. package/src/modules/plugins/plugin-rename.service.ts +21 -9
  980. package/src/modules/plugins/plugins.contract.ts +52 -4
  981. package/src/modules/plugins/plugins.routes.ts +204 -124
  982. package/src/modules/plugins/plugins.service.ts +92 -18
  983. package/src/modules/plugins/plugins.tools.ts +88 -0
  984. package/src/modules/plugins/teams.routes.ts +129 -0
  985. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  986. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  987. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  988. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  989. package/src/modules/secrets-vault/index.ts +1 -0
  990. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  991. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  992. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  993. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  994. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  995. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +188 -6
  996. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  997. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  998. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  999. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1000. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1001. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1002. package/src/modules/settings/connection-check.ts +351 -0
  1003. package/src/modules/settings/deployment-settings.service.ts +403 -57
  1004. package/src/modules/settings/git-root-folders.ts +171 -0
  1005. package/src/modules/settings/oidc-check.ts +229 -0
  1006. package/src/modules/settings/setup.routes.ts +456 -110
  1007. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1008. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1009. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1010. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1011. package/src/modules/skills/__tests__/skills.service.test.ts +223 -2
  1012. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1013. package/src/modules/skills/index.ts +4 -0
  1014. package/src/modules/skills/pending-skills.service.ts +21 -82
  1015. package/src/modules/skills/skill-access-requests.routes.ts +10 -6
  1016. package/src/modules/skills/skills.contract.ts +43 -11
  1017. package/src/modules/skills/skills.routes.ts +16 -1
  1018. package/src/modules/skills/skills.service.ts +214 -84
  1019. package/src/modules/skills/skills.tools.ts +43 -5
  1020. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1021. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1022. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
  1023. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1024. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1025. package/src/modules/tool-auth/external-api-key.interface.ts +56 -3
  1026. package/src/modules/tool-auth/external-api-key.service.ts +73 -25
  1027. package/src/modules/tool-auth/internal-token.service.ts +5 -2
  1028. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1029. package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
  1030. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1031. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1032. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1033. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1034. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1035. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1036. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1037. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1038. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1039. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1040. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +9 -4
  1041. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1042. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1043. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1044. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1045. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1046. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1047. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1048. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1049. package/src/modules/tool-manuals/index.ts +3 -0
  1050. package/src/modules/tool-manuals/mcp-json-discovery.ts +13 -10
  1051. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1052. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1053. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1054. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1055. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1056. package/src/modules/tool-manuals/tool-manuals.service.ts +432 -87
  1057. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1058. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1059. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1060. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1061. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1062. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1063. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1064. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1065. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +2 -0
  1066. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1067. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1068. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1069. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1070. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1071. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +2 -0
  1072. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1073. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +294 -25
  1074. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1075. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1076. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1077. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +14 -0
  1078. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +5 -0
  1079. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1080. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1081. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1082. package/src/modules/workflow/event-bus.ts +26 -28
  1083. package/src/modules/workflow/events.routes.ts +56 -21
  1084. package/src/modules/workflow/file-lock.service.ts +27 -4
  1085. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1086. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1087. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1088. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1089. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1090. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1091. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1092. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1093. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1094. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1095. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1096. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1097. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1098. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1099. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1100. package/src/modules/workflow/git/git-version.ts +5 -2
  1101. package/src/modules/workflow/git/git.service.ts +684 -144
  1102. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1103. package/src/modules/workflow/git/pull-request.service.ts +179 -27
  1104. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1105. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1106. package/src/modules/workflow/recovery-bot.ts +4 -1
  1107. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1108. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1109. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1110. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1111. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1112. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1113. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1114. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1115. package/src/modules/workflow/sanitize-error.ts +9 -4
  1116. package/src/modules/workflow/workflow.routes.ts +273 -29
  1117. package/src/modules/workflow/workflow.service.ts +1087 -171
  1118. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1119. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1120. package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
  1121. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1122. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1123. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1124. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1125. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1126. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1127. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1128. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1129. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1130. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +25 -6
  1131. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1132. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -5
  1133. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1134. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +56 -1
  1135. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1136. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1137. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1138. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1139. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1140. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1141. package/src/modules/workspace/empty-dirs.ts +79 -0
  1142. package/src/modules/workspace/file-readers/README.md +32 -0
  1143. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1144. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1145. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1146. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1147. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1148. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1149. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1150. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1151. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1152. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1153. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1154. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1155. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1156. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1157. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1158. package/src/modules/workspace/not-found.ts +119 -0
  1159. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1160. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1161. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1162. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1163. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1164. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1165. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1166. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1215 -73
  1167. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1168. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1169. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1170. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1171. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +46 -85
  1172. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1173. package/src/modules/workspace/startup/steps/seed-tree.ts +159 -78
  1174. package/src/modules/workspace/startup/steps/template-files.step.ts +492 -141
  1175. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1176. package/src/modules/workspace/workspace.routes.ts +687 -257
  1177. package/src/modules/workspace/workspace.service.ts +991 -303
  1178. package/src/modules/workspace/workspace.tools.ts +1787 -180
  1179. package/src/modules/workspace/write-denial.ts +189 -0
  1180. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1181. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1182. package/src/shared/__tests__/filename.test.ts +0 -0
  1183. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1184. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1185. package/src/shared/__tests__/git-internals.test.ts +115 -0
  1186. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1187. package/src/shared/__tests__/logging.test.ts +185 -0
  1188. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1189. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1190. package/src/shared/__tests__/printable.test.ts +64 -0
  1191. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1192. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1193. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1194. package/src/shared/canonical-file-identity.ts +92 -0
  1195. package/src/shared/domain-errors.ts +149 -0
  1196. package/src/shared/email-identity.ts +38 -0
  1197. package/src/shared/fs.contract.ts +225 -0
  1198. package/src/shared/git-failure.ts +196 -0
  1199. package/src/shared/git-internals.ts +124 -0
  1200. package/src/shared/git.contract.ts +170 -0
  1201. package/src/shared/http-errors.ts +36 -0
  1202. package/src/shared/logger.contract.ts +141 -0
  1203. package/src/shared/logging.ts +96 -0
  1204. package/src/shared/path-containment.ts +34 -0
  1205. package/src/shared/pending-proposals.ts +161 -0
  1206. package/src/shared/printable.ts +68 -0
  1207. package/src/shared/redact-secret.ts +118 -0
  1208. package/src/shared/rename-no-replace.ts +386 -0
  1209. package/src/shared/ssrf.ts +44 -16
  1210. package/src/version.ts +10 -6
  1211. package/src/modules/diff/diff-paths.ts +0 -9
  1212. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1213. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1214. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1215. package/src/shared/__tests__/fs-walk.test.ts +0 -63
  1216. package/src/shared/__tests__/kb-walk.test.ts +0 -107
  1217. package/src/shared/fs-errors.ts +0 -10
  1218. package/src/shared/fs-walk.ts +0 -45
  1219. package/src/shared/hash-email.ts +0 -14
  1220. package/src/shared/kb-walk.ts +0 -95
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { NodeFs } from '../../kb-fs/node-fs.js';
2
3
  import { execFile } from 'node:child_process';
3
4
  import { promisify } from 'node:util';
4
5
  import fs from 'node:fs/promises';
@@ -64,7 +65,7 @@ describe('WorkspaceService — branch-keyed identity', () => {
64
65
  });
65
66
 
66
67
  it('a malformed workspace id (bad percent-escape) rejects as a 400 BranchNameError, not a 500', async () => {
67
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
68
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
68
69
  // `%zz` fails decodeURIComponent, falls back to itself, and `%` never
69
70
  // passes the branch-name rules — the cold-path bootstrap must surface
70
71
  // that as the status-carrying domain error, not a wrapped plain Error.
@@ -76,7 +77,7 @@ describe('WorkspaceService — branch-keyed identity', () => {
76
77
 
77
78
  it('returns workspace info derived from the branch — no .workspace.json on disk', async () => {
78
79
  const { workspaceId, workspaceDir } = await seedBranchWorkspace(root, 'target-company-state');
79
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
80
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
80
81
  const info = await svc.getOrCreateForBranch('target-company-state');
81
82
 
82
83
  expect(info.id).toBe(workspaceId);
@@ -89,7 +90,7 @@ describe('WorkspaceService — branch-keyed identity', () => {
89
90
  it('two branches map to distinct directories', async () => {
90
91
  const a = await seedBranchWorkspace(root, 'target-company-state');
91
92
  const b = await seedBranchWorkspace(root, 'alice/feature');
92
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
93
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
93
94
 
94
95
  const infoA = await svc.getOrCreateForBranch('target-company-state');
95
96
  const infoB = await svc.getOrCreateForBranch('alice/feature');
@@ -101,22 +102,40 @@ describe('WorkspaceService — branch-keyed identity', () => {
101
102
  expect(infoB.absolutePath).toBe(path.join(root, 'alice%2Ffeature'));
102
103
  });
103
104
 
105
+ it('refuses a FILE squatting the clone path instead of wiping it, and leaves the bytes alone', async () => {
106
+ // The bootstrap's recovery — wipe and re-clone — is for a crashed
107
+ // bootstrap's directory shell. A regular file at the clone's name is a
108
+ // state a human put the deployment in, and ENOTDIR from the `.git` probe
109
+ // beneath it is absence like any other, so nothing distinguishes the two
110
+ // unless the clone path itself is checked. It must never be deleted.
111
+ const workspaceDir = path.join(root, workspaceIdForBranch('target-company-state'));
112
+ await fs.mkdir(workspaceDir, { recursive: true });
113
+ const squatter = path.join(workspaceDir, 'knowledge-base');
114
+ await fs.writeFile(squatter, 'not a clone', 'utf8');
115
+
116
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
117
+ await expect(svc.getOrCreateForBranch('target-company-state')).rejects.toThrow(
118
+ /"knowledge-base" in this workspace exists but is not a directory/,
119
+ );
120
+ expect(await fs.readFile(squatter, 'utf8')).toBe('not a clone');
121
+ });
122
+
104
123
  it('rejects invalid branch names before touching disk', async () => {
105
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
124
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
106
125
  await expect(svc.getOrCreateForBranch('')).rejects.toThrow();
107
126
  await expect(svc.getOrCreateForBranch('-bad-leading-dash')).rejects.toThrow();
108
127
  });
109
128
 
110
129
  it('getOrCreateForUser falls back to target-company-state when no branch is supplied', async () => {
111
130
  await seedBranchWorkspace(root, 'target-company-state');
112
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
131
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
113
132
  const info = await svc.getOrCreateForUser({ id: 'u', email: 'a@b.c', name: 'A' });
114
133
  expect(info.id).toBe(workspaceIdForBranch('target-company-state'));
115
134
  });
116
135
 
117
136
  it('getOrCreateForUser respects an explicit branch override', async () => {
118
137
  await seedBranchWorkspace(root, 'alice/feature');
119
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
138
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
120
139
  const info = await svc.getOrCreateForUser({ id: 'u', email: 'a@b.c', name: 'A' }, 'alice/feature');
121
140
  expect(info.id).toBe(workspaceIdForBranch('alice/feature'));
122
141
  });
@@ -126,14 +145,17 @@ describe('WorkspaceService.createDirectory', () => {
126
145
  let root: string;
127
146
  let svc: WorkspaceService;
128
147
  let workspaceDir: string;
148
+ /** Where a created folder goes: inside the checkout, whatever the caller spelled. */
149
+ let repoDir: string;
129
150
  let workspaceId: string;
130
151
 
131
152
  beforeEach(async () => {
132
153
  root = await mkTmpRoot();
133
154
  const seeded = await seedBranchWorkspace(root, 'target-company-state');
134
155
  workspaceDir = seeded.workspaceDir;
156
+ repoDir = path.join(workspaceDir, 'knowledge-base');
135
157
  workspaceId = seeded.workspaceId;
136
- svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
158
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
137
159
  // Hydrate the in-memory map so subsequent ops resolve fast.
138
160
  await svc.getWorkspacePath(workspaceId);
139
161
  });
@@ -142,9 +164,11 @@ describe('WorkspaceService.createDirectory', () => {
142
164
  await fs.rm(root, { recursive: true, force: true });
143
165
  });
144
166
 
145
- it('creates a .gitkeep inside a fresh empty folder', async () => {
167
+ it('creates a .gitkeep inside a fresh empty folder — in the repository, from an unprefixed path', async () => {
146
168
  await svc.createDirectory(workspaceId, 'a');
147
- const absDir = path.join(workspaceDir, 'a');
169
+ const absDir = path.join(repoDir, 'a');
170
+ // …and nothing beside the checkout, which is the whole point of the ticket.
171
+ await expect(fs.stat(path.join(workspaceDir, 'a'))).rejects.toMatchObject({ code: 'ENOENT' });
148
172
  const dirStat = await fs.stat(absDir);
149
173
  expect(dirStat.isDirectory()).toBe(true);
150
174
  const gitkeep = await fs.readFile(path.join(absDir, '.gitkeep'), 'utf-8');
@@ -152,7 +176,7 @@ describe('WorkspaceService.createDirectory', () => {
152
176
  });
153
177
 
154
178
  it('does not add a .gitkeep when the folder already has content', async () => {
155
- const absDir = path.join(workspaceDir, 'has-content');
179
+ const absDir = path.join(repoDir, 'has-content');
156
180
  await fs.mkdir(absDir, { recursive: true });
157
181
  await fs.writeFile(path.join(absDir, 'real.md'), 'hello', 'utf-8');
158
182
 
@@ -163,7 +187,7 @@ describe('WorkspaceService.createDirectory', () => {
163
187
  });
164
188
 
165
189
  it('does not duplicate a .gitkeep when one already exists', async () => {
166
- const absDir = path.join(workspaceDir, 'kept');
190
+ const absDir = path.join(repoDir, 'kept');
167
191
  await fs.mkdir(absDir, { recursive: true });
168
192
  await fs.writeFile(path.join(absDir, '.gitkeep'), '', 'utf-8');
169
193
 
@@ -176,9 +200,9 @@ describe('WorkspaceService.createDirectory', () => {
176
200
  it('only writes a .gitkeep in the leaf for nested paths', async () => {
177
201
  await svc.createDirectory(workspaceId, 'a/b/c');
178
202
 
179
- const aEntries = await fs.readdir(path.join(workspaceDir, 'a'));
180
- const bEntries = await fs.readdir(path.join(workspaceDir, 'a', 'b'));
181
- const cEntries = await fs.readdir(path.join(workspaceDir, 'a', 'b', 'c'));
203
+ const aEntries = await fs.readdir(path.join(repoDir, 'a'));
204
+ const bEntries = await fs.readdir(path.join(repoDir, 'a', 'b'));
205
+ const cEntries = await fs.readdir(path.join(repoDir, 'a', 'b', 'c'));
182
206
 
183
207
  expect(aEntries).toEqual(['b']);
184
208
  expect(bEntries).toEqual(['c']);
@@ -187,8 +211,8 @@ describe('WorkspaceService.createDirectory', () => {
187
211
 
188
212
  it('hides .gitkeep entries from listFiles', async () => {
189
213
  await svc.createDirectory(workspaceId, 'visible-empty');
190
- await fs.writeFile(path.join(workspaceDir, 'visible-empty', '.gitkeep'), '', 'utf-8');
191
- const mixed = path.join(workspaceDir, 'mixed');
214
+ await fs.writeFile(path.join(repoDir, 'visible-empty', '.gitkeep'), '', 'utf-8');
215
+ const mixed = path.join(repoDir, 'mixed');
192
216
  await fs.mkdir(mixed, { recursive: true });
193
217
  await fs.writeFile(path.join(mixed, '.gitkeep'), '', 'utf-8');
194
218
  await fs.writeFile(path.join(mixed, 'real.md'), 'hi', 'utf-8');
@@ -205,6 +229,418 @@ describe('WorkspaceService.createDirectory', () => {
205
229
  expect(names).toContain('visible-empty');
206
230
  expect(names).toContain('real.md');
207
231
  });
232
+
233
+ it('hides mcp-description.md when the root .bevelignore declares the platform rule', async () => {
234
+ const repoDir = path.join(workspaceDir, 'knowledge-base');
235
+ const nestedDir = path.join(repoDir, 'KnowledgeBase');
236
+ await fs.mkdir(nestedDir, { recursive: true });
237
+ await fs.writeFile(path.join(repoDir, '.bevelignore'), '/mcp-description.md\n', 'utf-8');
238
+ await fs.writeFile(path.join(repoDir, 'mcp-description.md'), 'Private deployment preamble', 'utf-8');
239
+ await fs.writeFile(path.join(nestedDir, 'mcp-description.md'), 'Ordinary nested knowledge', 'utf-8');
240
+ await fs.writeFile(path.join(repoDir, 'visible.md'), 'Visible knowledge', 'utf-8');
241
+
242
+ const tree = await svc.listFiles(workspaceId);
243
+ const repo = tree.children?.find((entry) => entry.name === 'knowledge-base');
244
+ const names = repo?.children?.map((entry) => entry.name) ?? [];
245
+
246
+ expect(names).toContain('visible.md');
247
+ expect(repo?.children?.find((entry) => entry.name === 'KnowledgeBase')?.children?.map((entry) => entry.name))
248
+ .toContain('mcp-description.md');
249
+ expect(names).not.toContain('mcp-description.md');
250
+ });
251
+ });
252
+
253
+ /**
254
+ * `writeFile`'s conditional write: the caller states the bytes it last read
255
+ * and the write is refused if the file no longer holds them. The compare and
256
+ * the write are one call so the route's per-path lock covers both.
257
+ */
258
+ describe('WorkspaceService.writeFile — expectedContent', () => {
259
+ let root: string;
260
+ let svc: WorkspaceService;
261
+ let workspaceDir: string;
262
+ let workspaceId: string;
263
+
264
+ beforeEach(async () => {
265
+ root = await mkTmpRoot();
266
+ const seeded = await seedBranchWorkspace(root, 'target-company-state');
267
+ workspaceDir = seeded.workspaceDir;
268
+ workspaceId = seeded.workspaceId;
269
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
270
+ });
271
+
272
+ afterEach(async () => {
273
+ await fs.rm(root, { recursive: true, force: true });
274
+ });
275
+
276
+ it('writes when the file still holds the expected content', async () => {
277
+ const rel = 'knowledge-base/mcp-description.md';
278
+ await fs.writeFile(path.join(workspaceDir, rel), 'Before.', 'utf-8');
279
+
280
+ await svc.writeFile(workspaceId, rel, 'After.', { expectedContent: 'Before.' });
281
+
282
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('After.');
283
+ });
284
+
285
+ it('refuses with a 409 and leaves the file alone when it changed underneath', async () => {
286
+ const rel = 'knowledge-base/mcp-description.md';
287
+ await fs.writeFile(path.join(workspaceDir, rel), "Another admin's text.", 'utf-8');
288
+
289
+ await expect(
290
+ svc.writeFile(workspaceId, rel, 'My stale merge.', { expectedContent: 'What I loaded.' }),
291
+ ).rejects.toMatchObject({ status: 409 });
292
+
293
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe("Another admin's text.");
294
+ });
295
+
296
+ it("treats an absent file as the empty string, so expecting '' creates it", async () => {
297
+ const rel = 'knowledge-base/mcp-description.md';
298
+
299
+ await svc.writeFile(workspaceId, rel, 'First write.', { expectedContent: '' });
300
+
301
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('First write.');
302
+ });
303
+
304
+ it('refuses to create over a file the caller believed absent', async () => {
305
+ const rel = 'knowledge-base/mcp-description.md';
306
+ await fs.writeFile(path.join(workspaceDir, rel), 'Seeded since the editor opened.', 'utf-8');
307
+
308
+ await expect(
309
+ svc.writeFile(workspaceId, rel, 'From an empty editor.', { expectedContent: '' }),
310
+ ).rejects.toMatchObject({ status: 409 });
311
+
312
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('Seeded since the editor opened.');
313
+ });
314
+
315
+ it('leaves no directory behind when it refuses', async () => {
316
+ // "A refused save leaves nothing behind" has to include the parent chain.
317
+ const rel = 'knowledge-base/new-folder/note.md';
318
+ await fs.writeFile(path.join(workspaceDir, 'knowledge-base', 'taken.md'), 'x', 'utf-8');
319
+
320
+ await expect(
321
+ svc.writeFile(workspaceId, rel, 'mine', { expectedContent: 'not what is there' }),
322
+ ).rejects.toMatchObject({ status: 409 });
323
+
324
+ await expect(fs.stat(path.join(workspaceDir, 'knowledge-base', 'new-folder'))).rejects.toMatchObject({
325
+ code: 'ENOENT',
326
+ });
327
+ });
328
+
329
+ it('still creates the parent chain for a save that goes ahead', async () => {
330
+ const rel = 'knowledge-base/new-folder/note.md';
331
+
332
+ await svc.writeFile(workspaceId, rel, 'mine', { expectedContent: '' });
333
+
334
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('mine');
335
+ });
336
+
337
+ it('refuses an absolute path rather than writing it inside the workspace', async () => {
338
+ // The property canonicalisation must not erode: `path.resolve` lets an
339
+ // absolute path win over the workspace dir, and the boundary check is what
340
+ // refuses it. A canonicaliser that dropped the empty leading segment would
341
+ // turn this into an ordinary write at `<workspace>/etc/passwd`.
342
+ //
343
+ // THE NORMALISER is what answers here, and its message names the path and
344
+ // a correction. It refuses the absolute path outright rather than placing
345
+ // it under the repository folder: an unprefixed `etc/passwd` is an ordinary
346
+ // page name, `/etc/passwd` is an attempt to leave, and the two must not be
347
+ // read as the same request.
348
+ await expect(
349
+ svc.writeFile(workspaceId, '/etc/passwd', 'pwned', { expectedContent: '' }),
350
+ ).rejects.toThrow('is outside the knowledge base repository');
351
+ });
352
+
353
+ it('is not applied at all when the option is absent', async () => {
354
+ const rel = 'knowledge-base/mcp-description.md';
355
+ await fs.writeFile(path.join(workspaceDir, rel), 'Whatever.', 'utf-8');
356
+
357
+ await svc.writeFile(workspaceId, rel, 'Replaced.');
358
+
359
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('Replaced.');
360
+ });
361
+
362
+ it('serializes writes per path, so two concurrent conditional saves cannot both pass', async () => {
363
+ // The compare and the write are two awaits apart. Without a turn per path
364
+ // both saves read the same bytes, both pass, and the loser's text is gone
365
+ // with no 409 to show for it.
366
+ const rel = 'knowledge-base/mcp-description.md';
367
+ await fs.writeFile(path.join(workspaceDir, rel), 'Before.', 'utf-8');
368
+
369
+ const [first, second] = await Promise.allSettled([
370
+ svc.writeFile(workspaceId, rel, 'From A.', { expectedContent: 'Before.' }),
371
+ svc.writeFile(workspaceId, rel, 'From B.', { expectedContent: 'Before.' }),
372
+ ]);
373
+
374
+ const settled = [first, second];
375
+ expect(settled.filter((r) => r.status === 'fulfilled')).toHaveLength(1);
376
+ const rejected = settled.find((r) => r.status === 'rejected') as PromiseRejectedResult;
377
+ expect(rejected.reason).toMatchObject({ status: 409 });
378
+ // Whichever won, the file holds ITS text whole — never a mix, never the loser's.
379
+ const landed = await fs.readFile(path.join(workspaceDir, rel), 'utf-8');
380
+ expect(landed).toBe(first.status === 'fulfilled' ? 'From A.' : 'From B.');
381
+ });
382
+
383
+ it('refuses a conditional write against a directory as a client error, not a raw failure', async () => {
384
+ // `GET /file` counts EISDIR as "no file at this path" and the editor opens
385
+ // empty on that 404, so the save that follows must say what is wrong.
386
+ await fs.mkdir(path.join(workspaceDir, 'knowledge-base', 'a-folder'), { recursive: true });
387
+
388
+ await expect(
389
+ svc.writeFile(workspaceId, 'knowledge-base/a-folder', 'text', { expectedContent: '' }),
390
+ ).rejects.toMatchObject({ status: 400, message: expect.stringContaining('is a directory') });
391
+ });
392
+ });
393
+
394
+ /**
395
+ * The per-path turn the conditional write rests on: what queues behind what,
396
+ * that two spellings of one file are one queue, and that a caller holding a
397
+ * turn can still call the ordinary write inside it.
398
+ */
399
+ describe('WorkspaceService.withPathTurn', () => {
400
+ let root: string;
401
+ let svc: WorkspaceService;
402
+ let workspaceDir: string;
403
+ let workspaceId: string;
404
+
405
+ beforeEach(async () => {
406
+ root = await mkTmpRoot();
407
+ const seeded = await seedBranchWorkspace(root, 'target-company-state');
408
+ workspaceDir = seeded.workspaceDir;
409
+ workspaceId = seeded.workspaceId;
410
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
411
+ });
412
+
413
+ afterEach(async () => {
414
+ await fs.rm(root, { recursive: true, force: true });
415
+ });
416
+
417
+ /** Yield to the microtask queue enough times that an unserialized body interleaves. */
418
+ const yieldTwice = async (): Promise<void> => {
419
+ await Promise.resolve();
420
+ await Promise.resolve();
421
+ };
422
+
423
+ it('runs one turn at a time, so two bodies never interleave', async () => {
424
+ const order: string[] = [];
425
+ const body = (name: string) => async (): Promise<void> => {
426
+ order.push(`${name}:enter`);
427
+ await yieldTwice();
428
+ order.push(`${name}:exit`);
429
+ };
430
+
431
+ // Warm the workspace lookup first, for the reason the next test gives:
432
+ // a COLD lookup resolves its callers in disk order rather than call
433
+ // order, so `b` can reach the queue first and this assertion — which
434
+ // names `a` as the one that goes first — fails for a scheduling reason
435
+ // rather than an interleaving one.
436
+ await svc.withPathTurn(workspaceId, 'knowledge-base/mcp-description.md', async () => undefined);
437
+
438
+ await Promise.all([
439
+ svc.withPathTurn(workspaceId, 'knowledge-base/mcp-description.md', body('a')),
440
+ svc.withPathTurn(workspaceId, 'knowledge-base/mcp-description.md', body('b')),
441
+ ]);
442
+
443
+ expect(order).toEqual(['a:enter', 'a:exit', 'b:enter', 'b:exit']);
444
+ });
445
+
446
+ it('treats two spellings of one file as one queue', async () => {
447
+ const order: string[] = [];
448
+ const body = (name: string) => async (): Promise<void> => {
449
+ order.push(`${name}:enter`);
450
+ await yieldTwice();
451
+ order.push(`${name}:exit`);
452
+ };
453
+
454
+ // Warm the workspace lookup first: a turn chains onto the queue only after
455
+ // it, and a COLD lookup resolves its three callers in disk order rather
456
+ // than call order — the queue is one either way, but this asserts FIFO.
457
+ await svc.withPathTurn(workspaceId, 'knowledge-base/mcp-description.md', async () => undefined);
458
+
459
+ await Promise.all([
460
+ svc.withPathTurn(workspaceId, 'knowledge-base/mcp-description.md', body('plain')),
461
+ svc.withPathTurn(workspaceId, './knowledge-base/mcp-description.md', body('dotted')),
462
+ svc.withPathTurn(workspaceId, 'knowledge-base//mcp-description.md', body('doubled')),
463
+ ]);
464
+
465
+ expect(order).toEqual([
466
+ 'plain:enter', 'plain:exit', 'dotted:enter', 'dotted:exit', 'doubled:enter', 'doubled:exit',
467
+ ]);
468
+ });
469
+
470
+ it('lets another file run while one path is held: a turn is per file', async () => {
471
+ let release: () => void = () => {};
472
+ const held = new Promise<void>((resolve) => {
473
+ release = resolve;
474
+ });
475
+ const holder = svc.withPathTurn(workspaceId, 'knowledge-base/one.md', () => held);
476
+
477
+ // Would hang if one file's turn queued another's.
478
+ await expect(
479
+ svc.withPathTurn(workspaceId, 'knowledge-base/two.md', async () => 'ran'),
480
+ ).resolves.toBe('ran');
481
+
482
+ release();
483
+ await holder;
484
+ });
485
+
486
+ it('is re-entrant: the write inside a held turn does not wait for itself', async () => {
487
+ const rel = 'knowledge-base/mcp-description.md';
488
+ await fs.writeFile(path.join(workspaceDir, rel), 'Before.', 'utf-8');
489
+
490
+ await svc.withPathTurn(workspaceId, rel, async () => {
491
+ // The same compare-and-write the route makes inside its own turn.
492
+ await svc.writeFile(workspaceId, rel, 'After.', { expectedContent: 'Before.' });
493
+ });
494
+
495
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('After.');
496
+ });
497
+
498
+ it('serializes two writes launched side by side INSIDE a held turn: one lands, the other is refused', async () => {
499
+ // Both inherit the held turn, so neither waits for it — but they must
500
+ // still take turns with each other, or both compare the same old
501
+ // content and the second silently overwrites the first.
502
+ const rel = 'knowledge-base/mcp-description.md';
503
+ const results = await svc.withPathTurn(workspaceId, rel, () =>
504
+ Promise.allSettled([
505
+ svc.writeFile(workspaceId, rel, 'From A.', { expectedContent: '' }),
506
+ svc.writeFile(workspaceId, rel, 'From B.', { expectedContent: '' }),
507
+ ]),
508
+ );
509
+ expect(results.filter((r) => r.status === 'fulfilled')).toHaveLength(1);
510
+ const refused = results.find((r) => r.status === 'rejected') as PromiseRejectedResult;
511
+ expect(refused.reason).toMatchObject({ status: 409 });
512
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('From A.');
513
+ });
514
+
515
+ it('updates the diff baseline inside the turn, in the order the mutations landed', async () => {
516
+ const rel = 'knowledge-base/notes.md';
517
+ await fs.writeFile(path.join(workspaceDir, rel), 'Old.', 'utf-8');
518
+ const absolute = path.join(workspaceDir, rel);
519
+ const onDisk = async () => fs.readFile(absolute, 'utf-8').catch(() => null);
520
+ // Each baseline update records what the file held when it ran. The
521
+ // property under test is that an update runs INSIDE its mutation's turn —
522
+ // after that mutation's disk effect and before the next mutation's — so
523
+ // the sequence of updates is the sequence of disk states.
524
+ const calls: Array<{ call: string; disk: string | null }> = [];
525
+ svc.setDiffService({
526
+ markUserDeleted: async () => {
527
+ calls.push({ call: 'deleted', disk: await onDisk() });
528
+ },
529
+ syncFromDisk: async () => {
530
+ calls.push({ call: 'synced', disk: await onDisk() });
531
+ },
532
+ } as never);
533
+ // A delete and a write launched together. Which takes the turn first is
534
+ // not promised (each resolves the workspace before acquiring), so the
535
+ // assertions hold for either order: every update saw its own mutation's
536
+ // effect, and the last update matches what is left on disk.
537
+ await Promise.all([svc.deleteFile(workspaceId, rel), svc.writeFile(workspaceId, rel, 'New.')]);
538
+ expect(calls.map((c) => c.call).sort()).toEqual(['deleted', 'synced']);
539
+ expect(calls.find((c) => c.call === 'deleted')?.disk).toBeNull();
540
+ expect(calls.find((c) => c.call === 'synced')?.disk).toBe('New.');
541
+ expect(await onDisk()).toBe(calls[1].disk);
542
+ });
543
+
544
+ it('a throwing turn does not wedge the ones queued behind it', async () => {
545
+ const rel = 'knowledge-base/mcp-description.md';
546
+
547
+ const failed = svc.withPathTurn(workspaceId, rel, async () => {
548
+ throw new Error('boom');
549
+ });
550
+ const after = svc.withPathTurn(workspaceId, rel, async () => 'ran');
551
+
552
+ await expect(failed).rejects.toThrow('boom');
553
+ await expect(after).resolves.toBe('ran');
554
+ });
555
+
556
+ it('holds a delete and an upload of the same path until the turn ends', async () => {
557
+ // The mutators cubic named: neither goes through writeFile, and either
558
+ // landing inside a conditional write's compare-then-write would defeat it.
559
+ const rel = 'knowledge-base/mcp-description.md';
560
+ const absolute = path.join(workspaceDir, rel);
561
+ await fs.writeFile(absolute, 'Held.', 'utf-8');
562
+ let release: () => void = () => {};
563
+ const held = new Promise<void>((resolve) => {
564
+ release = resolve;
565
+ });
566
+ // The turn is taken only after `withPathTurn` has resolved the workspace
567
+ // directory, and so are the contenders' — so calling first does not mean
568
+ // acquiring first. What the service promises is that whoever holds the
569
+ // turn holds it alone; the test must therefore wait until the holder
570
+ // actually HOLDS it before launching the delete and the upload, or one of
571
+ // them can win the acquisition and land first (a real 1-in-5 flake).
572
+ let holding: () => void = () => {};
573
+ const acquired = new Promise<void>((resolve) => {
574
+ holding = resolve;
575
+ });
576
+ const holder = svc.withPathTurn(workspaceId, rel, () => {
577
+ holding();
578
+ return held;
579
+ });
580
+ await acquired;
581
+
582
+ let deleted = false;
583
+ let uploaded = false;
584
+ const del = svc.deleteFile(workspaceId, rel).then(() => {
585
+ deleted = true;
586
+ });
587
+ const up = svc.writeFileBinary(workspaceId, rel, new Uint8Array([1, 2, 3])).then(() => {
588
+ uploaded = true;
589
+ });
590
+ // Real filesystem round trips, not setImmediate: an fs completion runs in
591
+ // the poll phase, which a check-phase callback can jump ahead of, so
592
+ // yielding by microtask would prove nothing about a free `fs.rm`.
593
+ for (let i = 0; i < 5; i += 1) await fs.stat(workspaceDir);
594
+
595
+ expect({ deleted, uploaded }).toEqual({ deleted: false, uploaded: false });
596
+ expect(await fs.readFile(absolute, 'utf-8')).toBe('Held.');
597
+
598
+ release();
599
+ await Promise.all([holder, del, up]);
600
+ expect({ deleted, uploaded }).toEqual({ deleted: true, uploaded: true });
601
+ });
602
+ });
603
+
604
+ describe('WorkspaceService.assertContentMatches', () => {
605
+ let root: string;
606
+ let svc: WorkspaceService;
607
+ let workspaceDir: string;
608
+ let workspaceId: string;
609
+
610
+ beforeEach(async () => {
611
+ root = await mkTmpRoot();
612
+ const seeded = await seedBranchWorkspace(root, 'target-company-state');
613
+ workspaceDir = seeded.workspaceDir;
614
+ workspaceId = seeded.workspaceId;
615
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
616
+ });
617
+
618
+ afterEach(async () => {
619
+ await fs.rm(root, { recursive: true, force: true });
620
+ });
621
+
622
+ it('passes silently when the file still holds the expected content', async () => {
623
+ const rel = 'knowledge-base/mcp-description.md';
624
+ await fs.writeFile(path.join(workspaceDir, rel), 'Before.', 'utf-8');
625
+
626
+ await expect(svc.assertContentMatches(workspaceId, rel, 'Before.')).resolves.toBeUndefined();
627
+ });
628
+
629
+ it('refuses a stale precondition with the write path\'s own 409, and writes nothing', async () => {
630
+ const rel = 'knowledge-base/mcp-description.md';
631
+ await fs.writeFile(path.join(workspaceDir, rel), "Another admin's text.", 'utf-8');
632
+
633
+ await expect(svc.assertContentMatches(workspaceId, rel, 'What I loaded.')).rejects.toMatchObject({
634
+ status: 409,
635
+ });
636
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe("Another admin's text.");
637
+ });
638
+
639
+ it('reads an absent file as the empty string', async () => {
640
+ await expect(
641
+ svc.assertContentMatches(workspaceId, 'knowledge-base/mcp-description.md', ''),
642
+ ).resolves.toBeUndefined();
643
+ });
208
644
  });
209
645
 
210
646
  describe('WorkspaceService.createFolderZip', () => {
@@ -212,13 +648,16 @@ describe('WorkspaceService.createFolderZip', () => {
212
648
  let svc: WorkspaceService;
213
649
  let workspaceDir: string;
214
650
  let workspaceId: string;
651
+ /** The folder a zip request names resolves inside the checkout. */
652
+ let repoDir: string;
215
653
 
216
654
  beforeEach(async () => {
217
655
  root = await mkTmpRoot();
218
656
  const seeded = await seedBranchWorkspace(root, 'target-company-state');
219
657
  workspaceDir = seeded.workspaceDir;
658
+ repoDir = path.join(workspaceDir, 'knowledge-base');
220
659
  workspaceId = seeded.workspaceId;
221
- svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
660
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
222
661
  await svc.getWorkspacePath(workspaceId);
223
662
  });
224
663
 
@@ -236,7 +675,7 @@ describe('WorkspaceService.createFolderZip', () => {
236
675
  }
237
676
 
238
677
  it('zips a folder prefixing entries with the folder name', async () => {
239
- const dir = path.join(workspaceDir, 'docs');
678
+ const dir = path.join(repoDir, 'docs');
240
679
  await fs.mkdir(dir, { recursive: true });
241
680
  await fs.writeFile(path.join(dir, 'a.md'), 'alpha');
242
681
  await fs.writeFile(path.join(dir, 'b.md'), 'beta');
@@ -250,7 +689,7 @@ describe('WorkspaceService.createFolderZip', () => {
250
689
  });
251
690
 
252
691
  it('preserves nested directory structure', async () => {
253
- const dir = path.join(workspaceDir, 'tree');
692
+ const dir = path.join(repoDir, 'tree');
254
693
  await fs.mkdir(path.join(dir, 'nested', 'deep'), { recursive: true });
255
694
  await fs.writeFile(path.join(dir, 'top.md'), 'top');
256
695
  await fs.writeFile(path.join(dir, 'nested', 'mid.md'), 'mid');
@@ -266,7 +705,7 @@ describe('WorkspaceService.createFolderZip', () => {
266
705
  });
267
706
 
268
707
  it('omits .git directories and .gitkeep files', async () => {
269
- const dir = path.join(workspaceDir, 'mixed');
708
+ const dir = path.join(repoDir, 'mixed');
270
709
  await fs.mkdir(path.join(dir, '.git', 'objects'), { recursive: true });
271
710
  await fs.writeFile(path.join(dir, '.git', 'config'), 'should-not-ship');
272
711
  await fs.writeFile(path.join(dir, '.git', 'objects', 'pack'), 'binary');
@@ -279,7 +718,7 @@ describe('WorkspaceService.createFolderZip', () => {
279
718
  });
280
719
 
281
720
  it('honors .bevelignore rules from inside the folder', async () => {
282
- const dir = path.join(workspaceDir, 'with-ignore');
721
+ const dir = path.join(repoDir, 'with-ignore');
283
722
  await fs.mkdir(dir, { recursive: true });
284
723
  await fs.writeFile(path.join(dir, '.bevelignore'), 'secret.md\n');
285
724
  await fs.writeFile(path.join(dir, 'public.md'), 'pub');
@@ -292,12 +731,19 @@ describe('WorkspaceService.createFolderZip', () => {
292
731
  });
293
732
 
294
733
  it('refuses to zip a file (not a directory)', async () => {
295
- await fs.writeFile(path.join(workspaceDir, 'lone.md'), 'one');
734
+ await fs.writeFile(path.join(repoDir, 'lone.md'), 'one');
296
735
  await expect(svc.createFolderZip(workspaceId, 'lone.md')).rejects.toThrow('Not a directory');
297
736
  });
298
737
 
299
- it('rejects path traversal outside the workspace', async () => {
300
- await expect(svc.createFolderZip(workspaceId, '../escape')).rejects.toThrow('Path traversal');
738
+ it('refuses a traversing path, and reads an unprefixed one as the repository folder', async () => {
739
+ await expect(svc.createFolderZip(workspaceId, '../escape')).rejects.toThrow(
740
+ 'is outside the knowledge base repository',
741
+ );
742
+ // The unprefixed spelling is the repository's own folder, so it zips.
743
+ await fs.mkdir(path.join(repoDir, 'unprefixed'), { recursive: true });
744
+ await fs.writeFile(path.join(repoDir, 'unprefixed', 'a.md'), 'a');
745
+ const buf = await svc.createFolderZip(workspaceId, 'unprefixed');
746
+ expect((await unzipEntries(buf)).map((e) => e.name)).toEqual(['unprefixed/a.md']);
301
747
  });
302
748
 
303
749
  it('throws FolderTooLargeError when contents exceed the size cap', async () => {
@@ -310,7 +756,7 @@ describe('WorkspaceService.createFolderZip', () => {
310
756
  // in diff.service.seed-atomicity.test.ts. A re-imported namespace via
311
757
  // `await import('node:fs/promises')` is sealed and `vi.spyOn` can't
312
758
  // redefine its properties.
313
- const dir = path.join(workspaceDir, 'too-big');
759
+ const dir = path.join(repoDir, 'too-big');
314
760
  await fs.mkdir(dir, { recursive: true });
315
761
  const filePath = path.join(dir, 'huge.bin');
316
762
  await fs.writeFile(filePath, 'x'); // 1 byte real; stat will lie below.
@@ -367,7 +813,7 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
367
813
  });
368
814
 
369
815
  it('clones a branch on first bootstrap and checks out the right ref', async () => {
370
- const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
816
+ const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base', new NodeFs());
371
817
  const info = await svc.getOrCreateForBranch('target-company-state');
372
818
 
373
819
  const repo = path.join(info.absolutePath, 'knowledge-base');
@@ -376,7 +822,7 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
376
822
  });
377
823
 
378
824
  it('uses an existing sibling clone as a --reference and stays dissociated', async () => {
379
- const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
825
+ const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base', new NodeFs());
380
826
  // First branch: plain clone — becomes the sibling for the next bootstrap.
381
827
  await svc.getOrCreateForBranch('target-company-state');
382
828
  // Second branch: should borrow the first clone's objects, then dissociate.
@@ -404,7 +850,7 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
404
850
  // post-merge pull of a target branch. Every clone this service hands out must
405
851
  // therefore track exactly one upstream ref through exactly one refspec.
406
852
  it('stamps a single fetch refspec and upstream ref on a fresh clone', async () => {
407
- const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
853
+ const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base', new NodeFs());
408
854
  const info = await svc.getOrCreateForBranch('target-company-state');
409
855
  const repo = path.join(info.absolutePath, 'knowledge-base');
410
856
 
@@ -428,7 +874,7 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
428
874
  await runGit(repo, ['config', '--add', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*']);
429
875
  await runGit(repo, ['config', '--add', 'branch.alice/draft.merge', 'refs/heads/target-company-state']);
430
876
 
431
- const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
877
+ const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base', new NodeFs());
432
878
  await svc.getOrCreateForBranch('alice/draft');
433
879
 
434
880
  expect(await gitOut(repo, ['config', '--get-all', 'remote.origin.fetch']))
@@ -440,7 +886,7 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
440
886
  });
441
887
 
442
888
  it('notifies the cloned-workspace listener with the workspace id after a clone', async () => {
443
- const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base');
889
+ const svc = new WorkspaceService(workspacesRoot, upstream, 'knowledge-base', new NodeFs());
444
890
  const cloned: string[] = [];
445
891
  svc.setWorkspaceClonedListener((id) => cloned.push(id));
446
892
 
@@ -449,6 +895,126 @@ describe('WorkspaceService — clone bootstrap & sibling reference', () => {
449
895
  });
450
896
  });
451
897
 
898
+ /**
899
+ * Reads and writes never follow a symbolic link — as the file itself, or as
900
+ * a directory on the way to it. A link only reaches a repository by direct
901
+ * git push, and following one would hand out (or overwrite) whatever the
902
+ * server process can reach. The workspace root behind a link of the
903
+ * operator's is the one link that is fine.
904
+ */
905
+ describe('WorkspaceService — symbolic links', () => {
906
+ let root: string;
907
+ let svc: WorkspaceService;
908
+ let workspaceDir: string;
909
+ let workspaceId: string;
910
+ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
911
+
912
+ beforeEach(async () => {
913
+ root = await mkTmpRoot();
914
+ const seeded = await seedBranchWorkspace(root, 'main');
915
+ workspaceDir = seeded.workspaceDir;
916
+ workspaceId = seeded.workspaceId;
917
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
918
+ });
919
+
920
+ afterEach(async () => {
921
+ await fs.rm(root, { recursive: true, force: true });
922
+ });
923
+
924
+ it('refuses to read or write a file that is a link, and leaves the target untouched', async () => {
925
+ const secret = path.join(root, 'secret.txt');
926
+ await fs.writeFile(secret, 'DATABASE_URL=postgres://…', 'utf-8');
927
+ const rel = 'knowledge-base/mcp-description.md';
928
+ await fs.symlink(secret, path.join(workspaceDir, rel));
929
+
930
+ await expect(svc.readFile(workspaceId, rel)).rejects.toThrow('Path traversal detected');
931
+ await expect(svc.readFileBinary(workspaceId, rel)).rejects.toThrow('Path traversal detected');
932
+ await expect(svc.writeFile(workspaceId, rel, 'Public text.')).rejects.toThrow('Path traversal detected');
933
+ await expect(svc.writeFileBinary(workspaceId, rel, Buffer.from('x'))).rejects.toThrow('Path traversal detected');
934
+ expect(await fs.readFile(secret, 'utf-8')).toBe('DATABASE_URL=postgres://…');
935
+ });
936
+
937
+ it('refuses a file reached through a linked directory, existing or yet to be created', async () => {
938
+ const elsewhere = path.join(root, 'elsewhere');
939
+ await fs.mkdir(elsewhere, { recursive: true });
940
+ await fs.writeFile(path.join(elsewhere, 'note.md'), 'outside', 'utf-8');
941
+ await fs.symlink(elsewhere, path.join(workspaceDir, 'knowledge-base', 'linked'), linkType);
942
+
943
+ await expect(svc.readFile(workspaceId, 'knowledge-base/linked/note.md')).rejects.toThrow('Path traversal detected');
944
+ await expect(svc.writeFile(workspaceId, 'knowledge-base/linked/new/deeper.md', 'x')).rejects.toThrow(
945
+ 'Path traversal detected',
946
+ );
947
+ expect(await fs.readdir(elsewhere)).toEqual(['note.md']);
948
+ });
949
+
950
+ it('refuses a path through a DANGLING link — its absence is not leave to climb past it', async () => {
951
+ // A link to nothing resolves to nothing, like a missing folder would; a
952
+ // guard that then climbed to the parent would pass, and the write would
953
+ // land wherever the link is pointed at by the time it runs.
954
+ // A junction needs an existing target, so on Windows the dangling link is
955
+ // a file link — it may point at nothing, and lstat still reports a link.
956
+ await fs.symlink(
957
+ path.join(root, 'nowhere'),
958
+ path.join(workspaceDir, 'knowledge-base', 'dangling'),
959
+ process.platform === 'win32' ? 'file' : 'dir',
960
+ );
961
+ await expect(svc.writeFile(workspaceId, 'knowledge-base/dangling/note.md', 'x')).rejects.toThrow(
962
+ 'Path traversal detected',
963
+ );
964
+ await expect(fs.access(path.join(root, 'nowhere'))).rejects.toThrow();
965
+ });
966
+
967
+ it('refuses a directory, a move and an extraction that would go through a link', async () => {
968
+ const elsewhere = path.join(root, 'elsewhere');
969
+ await fs.mkdir(elsewhere, { recursive: true });
970
+ await fs.symlink(elsewhere, path.join(workspaceDir, 'knowledge-base', 'linked'), linkType);
971
+ await svc.writeFile(workspaceId, 'knowledge-base/real.md', 'real');
972
+
973
+ await expect(svc.createDirectory(workspaceId, 'knowledge-base/linked/new-folder')).rejects.toThrow(
974
+ 'Path traversal detected',
975
+ );
976
+ await expect(svc.moveEntry(workspaceId, 'knowledge-base/real.md', 'knowledge-base/linked/real.md')).rejects.toThrow(
977
+ 'Path traversal detected',
978
+ );
979
+ expect(await fs.readFile(path.join(workspaceDir, 'knowledge-base', 'real.md'), 'utf-8')).toBe('real');
980
+
981
+ // An archive whose entry lands under the link: that entry is skipped and
982
+ // reported; the rest extracts.
983
+ const { default: AdmZip } = await import('adm-zip');
984
+ const zip = new AdmZip();
985
+ zip.addFile('linked/escaped.md', Buffer.from('out'));
986
+ zip.addFile('kept.md', Buffer.from('in'));
987
+ await fs.writeFile(path.join(workspaceDir, 'knowledge-base', 'a.zip'), zip.toBuffer());
988
+ const res = await svc.unzipFile(workspaceId, 'knowledge-base/a.zip', 'knowledge-base');
989
+ expect(res.extracted).toEqual(['knowledge-base/kept.md']);
990
+ expect(res.skipped).toContainEqual({ path: 'linked/escaped.md', reason: 'Path traversal detected' });
991
+ expect(await fs.readdir(elsewhere)).toEqual([]);
992
+ });
993
+
994
+ it('reads and writes as before when nothing on the path is a link, the workspace root behind one included', async () => {
995
+ const rel = 'knowledge-base/Folder/new/page.md';
996
+ await svc.writeFile(workspaceId, rel, 'Hello.');
997
+ expect(await svc.readFile(workspaceId, rel)).toBe('Hello.');
998
+
999
+ // A mounted-volume shape: the whole workspaces root reached through a link.
1000
+ const mount = path.join(root, 'mount');
1001
+ await fs.symlink(root, mount, linkType);
1002
+ const viaMount = new WorkspaceService(mount, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
1003
+ expect(await viaMount.readFile(workspaceId, rel)).toBe('Hello.');
1004
+ await viaMount.writeFile(workspaceId, rel, 'Hello again.');
1005
+ expect(await fs.readFile(path.join(workspaceDir, rel), 'utf-8')).toBe('Hello again.');
1006
+
1007
+ // One workspace directory mounted elsewhere: the directory itself is a
1008
+ // link of the operator's, and everything beneath it is its own.
1009
+ const elsewhere = path.join(root, 'elsewhere-ws');
1010
+ await fs.rename(workspaceDir, elsewhere);
1011
+ await fs.symlink(elsewhere, workspaceDir, linkType);
1012
+ expect(await svc.readFile(workspaceId, rel)).toBe('Hello again.');
1013
+ await svc.writeFile(workspaceId, rel, 'Third.');
1014
+ expect(await fs.readFile(path.join(elsewhere, rel), 'utf-8')).toBe('Third.');
1015
+ });
1016
+ });
1017
+
452
1018
  describe('WorkspaceService.sweepOrphanedWorkspaces', () => {
453
1019
  let root: string;
454
1020
 
@@ -466,7 +1032,7 @@ describe('WorkspaceService.sweepOrphanedWorkspaces', () => {
466
1032
 
467
1033
  it('removes the clone of a branch that is not in the known set', async () => {
468
1034
  const { workspaceId, workspaceDir } = await seedBranchWorkspace(root, 'target-company-state');
469
- const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
1035
+ const svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
470
1036
  await svc.getOrCreateForBranch('target-company-state');
471
1037
 
472
1038
  // The branch vanishes from the known set; the sweep reclaims its clone.
@@ -487,7 +1053,7 @@ describe('WorkspaceService.readAllKbFiles', () => {
487
1053
  const seeded = await seedBranchWorkspace(root, 'target-company-state');
488
1054
  repoRoot = path.join(seeded.workspaceDir, 'knowledge-base');
489
1055
  workspaceId = seeded.workspaceId;
490
- svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
1056
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
491
1057
  await svc.getWorkspacePath(workspaceId);
492
1058
  });
493
1059
 
@@ -536,14 +1102,17 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
536
1102
  let root: string;
537
1103
  let svc: WorkspaceService;
538
1104
  let workspaceDir: string;
1105
+ /** Archive and destination alike resolve inside the checkout. */
1106
+ let repoDir: string;
539
1107
  let workspaceId: string;
540
1108
 
541
1109
  beforeEach(async () => {
542
1110
  root = await mkTmpRoot();
543
1111
  const seeded = await seedBranchWorkspace(root, 'target-company-state');
544
1112
  workspaceDir = seeded.workspaceDir;
1113
+ repoDir = path.join(workspaceDir, 'knowledge-base');
545
1114
  workspaceId = seeded.workspaceId;
546
- svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base');
1115
+ svc = new WorkspaceService(root, 'https://github.com/Bevel-Software/knowledge-base.git', 'knowledge-base', new NodeFs());
547
1116
  await svc.getWorkspacePath(workspaceId);
548
1117
  });
549
1118
 
@@ -555,7 +1124,7 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
555
1124
  const { default: AdmZip } = await import('adm-zip');
556
1125
  const zip = new AdmZip();
557
1126
  for (const [name, content] of Object.entries(files)) zip.addFile(name, Buffer.from(content));
558
- await fs.writeFile(path.join(workspaceDir, rel), zip.toBuffer());
1127
+ await fs.writeFile(path.join(repoDir, rel), zip.toBuffer());
559
1128
  }
560
1129
 
561
1130
  it('skips entries the write guard rejects and never writes them to disk', async () => {
@@ -563,10 +1132,13 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
563
1132
  const res = await svc.unzipFile(workspaceId, 'a.zip', 'out', async (wsPath) => {
564
1133
  if (wsPath.endsWith('blocked.md')) throw new Error('Blocked by the ontology-session boundary');
565
1134
  });
566
- expect(res.extracted).toEqual(['out/keep.md']);
1135
+ // An unprefixed destination is the repository's own `out/`, and the
1136
+ // extracted paths are reported workspace-relative, so prefixed.
1137
+ expect(res.extracted).toEqual(['knowledge-base/out/keep.md']);
567
1138
  expect(res.skipped).toContainEqual({ path: 'blocked.md', reason: 'Blocked by the ontology-session boundary' });
568
- expect((await fs.readFile(path.join(workspaceDir, 'out', 'keep.md'))).toString()).toBe('ok');
569
- await expect(fs.readFile(path.join(workspaceDir, 'out', 'blocked.md'))).rejects.toThrow();
1139
+ expect((await fs.readFile(path.join(repoDir, 'out', 'keep.md'))).toString()).toBe('ok');
1140
+ await expect(fs.readFile(path.join(repoDir, 'out', 'blocked.md'))).rejects.toThrow();
1141
+ await expect(fs.stat(path.join(workspaceDir, 'out'))).rejects.toMatchObject({ code: 'ENOENT' });
570
1142
  });
571
1143
 
572
1144
  it('skips a guard-rejected directory entry without creating it on disk', async () => {
@@ -574,15 +1146,15 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
574
1146
  const zip = new AdmZip();
575
1147
  zip.addFile('blocked-dir/', Buffer.alloc(0)); // bare directory entry
576
1148
  zip.addFile('keep.md', Buffer.from('ok'));
577
- await fs.writeFile(path.join(workspaceDir, 'c.zip'), zip.toBuffer());
1149
+ await fs.writeFile(path.join(repoDir, 'c.zip'), zip.toBuffer());
578
1150
 
579
1151
  const res = await svc.unzipFile(workspaceId, 'c.zip', 'out', async (wsPath) => {
580
1152
  if (wsPath.includes('blocked-dir')) throw new Error('Blocked by the ontology-session boundary');
581
1153
  });
582
1154
 
583
- expect(res.extracted).toEqual(['out/keep.md']);
1155
+ expect(res.extracted).toEqual(['knowledge-base/out/keep.md']);
584
1156
  expect(res.skipped).toContainEqual({ path: 'blocked-dir/', reason: 'Blocked by the ontology-session boundary' });
585
- await expect(fs.stat(path.join(workspaceDir, 'out', 'blocked-dir'))).rejects.toThrow();
1157
+ await expect(fs.stat(path.join(repoDir, 'out', 'blocked-dir'))).rejects.toThrow();
586
1158
  });
587
1159
 
588
1160
  it('does not create the destination directory when every entry is blocked', async () => {
@@ -592,13 +1164,210 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
592
1164
  });
593
1165
  expect(res.extracted).toEqual([]);
594
1166
  expect(res.skipped).toHaveLength(2);
595
- await expect(fs.stat(path.join(workspaceDir, 'out'))).rejects.toThrow();
1167
+ await expect(fs.stat(path.join(repoDir, 'out'))).rejects.toThrow();
1168
+ });
1169
+
1170
+ // The zip reader cannot tell a missing file from a corrupt one: it answers
1171
+ // "ADM-ZIP: Invalid filename" for both, and unzip used to dress that up as a
1172
+ // 422 unreadable archive — blaming bytes that were never there. Absence is
1173
+ // asked first, so the tool can give it the one 404 every file tool gives.
1174
+ it('a .zip that is not there is a 404 not_found, not an unreadable archive', async () => {
1175
+ await expect(svc.unzipFile(workspaceId, 'nope.zip')).rejects.toMatchObject({
1176
+ name: 'PathNotFoundError',
1177
+ status: 404,
1178
+ payload: { kind: 'not_found', path: 'knowledge-base/nope.zip' },
1179
+ });
1180
+ });
1181
+
1182
+ // The error carries the WHOLE answer — sanitized path, kind AND the next
1183
+ // step — because not every surface re-renders it. `POST /workspace/:id/unzip`
1184
+ // hands this straight to `domainErrorBody`, so whatever is missing here is
1185
+ // missing from the HTTP 404 that the tools' 404 is supposed to match.
1186
+ it('the missing-archive 404 carries the next step and a one-line path, on any surface', async () => {
1187
+ await expect(svc.unzipFile(workspaceId, 'nope.zip')).rejects.toMatchObject({
1188
+ message:
1189
+ 'There is no file or directory at "knowledge-base/nope.zip" in this workspace. Check the path with list_files.',
1190
+ });
1191
+ // A name carrying a line break cannot forge a second line of the answer,
1192
+ // in the message or in the `path` a JSON consumer reads.
1193
+ const forged = 'a\nb\u2028c.zip';
1194
+ await expect(svc.unzipFile(workspaceId, forged)).rejects.toMatchObject({
1195
+ status: 404,
1196
+ payload: { kind: 'not_found', path: 'knowledge-base/a\\nb\\u2028c.zip' },
1197
+ });
1198
+ });
1199
+
1200
+ // The archive is read ONCE and the reader is handed those bytes. With a
1201
+ // `stat` probe followed by `new AdmZip(path)` there were two moments: an
1202
+ // archive that vanished in between passed the probe and then failed the
1203
+ // open, and the caller got back the 422 the probe existed to prevent.
1204
+ it('an archive that vanishes as it is opened still answers 404, never the 422', async () => {
1205
+ await writeZip('racy.zip', { 'x.md': '1' });
1206
+ const gone = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
1207
+ const readSpy = vi.spyOn(fs, 'readFile').mockRejectedValueOnce(gone);
1208
+ try {
1209
+ await expect(svc.unzipFile(workspaceId, 'racy.zip')).rejects.toMatchObject({
1210
+ name: 'PathNotFoundError',
1211
+ status: 404,
1212
+ payload: { kind: 'not_found', path: 'knowledge-base/racy.zip' },
1213
+ });
1214
+ } finally {
1215
+ readSpy.mockRestore();
1216
+ }
1217
+ });
1218
+
1219
+ // The other half of that single read: bytes that ARE there but are not a zip
1220
+ // are still the archive's problem, not the path's.
1221
+ it('a file that is not a zip is still the 422, read from its bytes', async () => {
1222
+ await fs.writeFile(path.join(repoDir, 'junk.zip'), 'not really a zip');
1223
+ await expect(svc.unzipFile(workspaceId, 'junk.zip')).rejects.toMatchObject({
1224
+ status: 422,
1225
+ payload: { kind: 'unreadable-archive' },
1226
+ });
596
1227
  });
597
1228
 
598
1229
  it('extracts everything when no guard is supplied (human / non-agent path)', async () => {
599
1230
  await writeZip('b.zip', { 'x.md': '1', 'y.md': '2' });
600
1231
  const res = await svc.unzipFile(workspaceId, 'b.zip', 'out');
601
- expect(res.extracted.sort()).toEqual(['out/x.md', 'out/y.md']);
1232
+ expect(res.extracted.sort()).toEqual(['knowledge-base/out/x.md', 'knowledge-base/out/y.md']);
602
1233
  expect(res.skipped).toEqual([]);
603
1234
  });
604
1235
  });
1236
+
1237
+ /**
1238
+ * The service resolves a workspace path in ONE place, and that place does two
1239
+ * things: it places the path inside the repository, and it checks the RESOLVED
1240
+ * location against `<workspaceDir>/<kbDirName>`. Nine inline
1241
+ * `path.resolve(workspaceDir, …)` calls used to do the first half only — which
1242
+ * is how an unprefixed path written by a staging test ended up beside the
1243
+ * checkout, never committed and never pushed.
1244
+ */
1245
+ describe('WorkspaceService — every operation resolves inside the repository', () => {
1246
+ let root: string;
1247
+ let svc: WorkspaceService;
1248
+ let workspaceDir: string;
1249
+ let repoDir: string;
1250
+ let workspaceId: string;
1251
+
1252
+ beforeEach(async () => {
1253
+ root = await mkTmpRoot();
1254
+ const seeded = await seedBranchWorkspace(root, 'target-company-state');
1255
+ workspaceDir = seeded.workspaceDir;
1256
+ repoDir = path.join(workspaceDir, 'knowledge-base');
1257
+ workspaceId = seeded.workspaceId;
1258
+ svc = new WorkspaceService(root, 'https://example.invalid/kb.git', 'knowledge-base', new NodeFs());
1259
+ await svc.getWorkspacePath(workspaceId);
1260
+ });
1261
+
1262
+ afterEach(async () => {
1263
+ await fs.rm(root, { recursive: true, force: true });
1264
+ });
1265
+
1266
+ /** Whatever sits in the workspace directory other than the checkout. */
1267
+ const besideCheckout = async () => (await fs.readdir(workspaceDir)).filter((n) => n !== 'knowledge-base').sort();
1268
+
1269
+ it('puts every write an unprefixed path names inside the checkout', async () => {
1270
+ await svc.writeFile(workspaceId, 'KnowledgeBase/Report.md', 'body');
1271
+ await svc.writeFileBinary(workspaceId, 'Uploads/TestDocx.docx', Buffer.from('bytes'));
1272
+ await svc.createDirectory(workspaceId, 'Plugins/GTM');
1273
+
1274
+ expect(await fs.readFile(path.join(repoDir, 'KnowledgeBase', 'Report.md'), 'utf-8')).toBe('body');
1275
+ expect(await fs.readFile(path.join(repoDir, 'Uploads', 'TestDocx.docx'), 'utf-8')).toBe('bytes');
1276
+ expect((await fs.stat(path.join(repoDir, 'Plugins', 'GTM'))).isDirectory()).toBe(true);
1277
+ expect(await besideCheckout()).toEqual([]);
1278
+ });
1279
+
1280
+ it('reads, moves and deletes the repository file an unprefixed path names', async () => {
1281
+ await svc.writeFile(workspaceId, 'a.md', 'one');
1282
+
1283
+ expect(await svc.readFile(workspaceId, 'a.md')).toBe('one');
1284
+ expect((await svc.readFileBinary(workspaceId, 'a.md')).toString()).toBe('one');
1285
+ // Both spellings are the same file.
1286
+ expect(await svc.readFile(workspaceId, 'knowledge-base/a.md')).toBe('one');
1287
+
1288
+ await svc.moveEntry(workspaceId, 'a.md', 'Moved/a.md');
1289
+ expect(await fs.readFile(path.join(repoDir, 'Moved', 'a.md'), 'utf-8')).toBe('one');
1290
+
1291
+ await svc.deleteFile(workspaceId, 'Moved/a.md');
1292
+ await expect(fs.stat(path.join(repoDir, 'Moved', 'a.md'))).rejects.toMatchObject({ code: 'ENOENT' });
1293
+ expect(await besideCheckout()).toEqual([]);
1294
+ });
1295
+
1296
+ it('takes its path and folder turns on the repository path, so two spellings are one queue', async () => {
1297
+ const order: string[] = [];
1298
+ const slow = svc.withPathTurn(workspaceId, 'contended.md', async () => {
1299
+ await new Promise((r) => setTimeout(r, 20));
1300
+ order.push('first');
1301
+ });
1302
+ const fast = svc.withPathTurn(workspaceId, 'knowledge-base/contended.md', async () => {
1303
+ order.push('second');
1304
+ });
1305
+ await Promise.all([slow, fast]);
1306
+ expect(order).toEqual(['first', 'second']);
1307
+
1308
+ await svc.withFolderTurn(workspaceId, 'Folder', async () => undefined);
1309
+ });
1310
+
1311
+ it('refuses a path that resolves outside the checkout after normalisation', async () => {
1312
+ // The spelling is contained and normalises into the repository, so the
1313
+ // LEXICAL root check in `resolveInsideRepo` passes it — `knowledge-base/
1314
+ // Escape/x.md` stays under the repository however `Escape` resolves. What
1315
+ // refuses it is `assertNotThroughLink`, which follows the link. Both run
1316
+ // after normalisation, and this pins the one that can actually fire on a
1317
+ // spelling the normaliser accepts.
1318
+ const outside = path.join(workspaceDir, 'outside');
1319
+ await fs.mkdir(outside, { recursive: true });
1320
+ await fs.symlink(outside, path.join(repoDir, 'Escape'), process.platform === 'win32' ? 'junction' : 'dir');
1321
+
1322
+ await expect(svc.writeFile(workspaceId, 'Escape/x.md', 'x')).rejects.toThrow('Path traversal detected');
1323
+ await expect(svc.readFile(workspaceId, 'Escape/x.md')).rejects.toThrow('Path traversal detected');
1324
+ expect(await fs.readdir(outside)).toEqual([]);
1325
+ });
1326
+
1327
+ it('cannot be asked for the workspace directory itself, or anything beside the checkout', async () => {
1328
+ // The workspace directory has no spelling left: every path names something
1329
+ // under the checkout, and the ones that used to reach up are refused.
1330
+ for (const dir of ['', '.', '..', '/', 'knowledge-base/..']) {
1331
+ await expect(svc.readFile(workspaceId, dir)).rejects.toThrow(
1332
+ 'is outside the knowledge base repository',
1333
+ );
1334
+ }
1335
+ await fs.writeFile(path.join(workspaceDir, 'stray.md'), 'already there', 'utf-8');
1336
+ // `stray.md` names the REPOSITORY's `stray.md` now — the one beside the
1337
+ // checkout has no path that reaches it, which is the point.
1338
+ await expect(svc.readFile(workspaceId, 'stray.md')).rejects.toMatchObject({ code: 'ENOENT' });
1339
+ await svc.writeFile(workspaceId, 'stray.md', 'in the repository');
1340
+ expect(await fs.readFile(path.join(workspaceDir, 'stray.md'), 'utf-8')).toBe('already there');
1341
+ expect(await fs.readFile(path.join(repoDir, 'stray.md'), 'utf-8')).toBe('in the repository');
1342
+ });
1343
+
1344
+ it('reserves the checkout folder name at the repository root on create, move and write', async () => {
1345
+ const reserved = /"knowledge-base" is reserved/;
1346
+ await expect(svc.createDirectory(workspaceId, 'knowledge-base/knowledge-base')).rejects.toThrow(reserved);
1347
+ await expect(svc.writeFile(workspaceId, 'knowledge-base/knowledge-base/x.md', 'x')).rejects.toThrow(reserved);
1348
+ await expect(
1349
+ svc.writeFileBinary(workspaceId, 'knowledge-base/knowledge-base/x.bin', Buffer.from('x')),
1350
+ ).rejects.toThrow(reserved);
1351
+
1352
+ await svc.writeFile(workspaceId, 'movable.md', 'm');
1353
+ await expect(svc.moveEntry(workspaceId, 'movable.md', 'knowledge-base/knowledge-base/movable.md')).rejects.toThrow(
1354
+ reserved,
1355
+ );
1356
+ await expect(fs.stat(path.join(repoDir, 'knowledge-base'))).rejects.toMatchObject({ code: 'ENOENT' });
1357
+
1358
+ // The refusal says why, so nobody has to guess.
1359
+ await expect(svc.createDirectory(workspaceId, 'knowledge-base/knowledge-base')).rejects.toThrow(
1360
+ /it is the checkout folder's name/,
1361
+ );
1362
+ // A case variant is the same folder wherever the repository is cloned onto
1363
+ // a case-insensitive filesystem, so it is reserved in every spelling.
1364
+ await expect(svc.createDirectory(workspaceId, 'Knowledge-Base')).rejects.toThrow(/"Knowledge-Base" is reserved/);
1365
+ await expect(svc.writeFile(workspaceId, 'KNOWLEDGE-BASE/x.md', 'x')).rejects.toThrow(
1366
+ /refused in every spelling/,
1367
+ );
1368
+
1369
+ // A namesake deeper in the tree is ordinary content.
1370
+ await svc.createDirectory(workspaceId, 'KnowledgeBase/knowledge-base');
1371
+ expect((await fs.stat(path.join(repoDir, 'KnowledgeBase', 'knowledge-base'))).isDirectory()).toBe(true);
1372
+ });
1373
+ });