@bevel-software/platform-core-backend 0.1.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 (720) hide show
  1. package/LICENSE +202 -0
  2. package/dist/assets.d.ts +5 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +23 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/core/core-ports.d.ts +83 -0
  7. package/dist/core/core-ports.d.ts.map +1 -0
  8. package/dist/core/core-ports.js +12 -0
  9. package/dist/core/core-ports.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +62 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -0
  12. package/dist/core/create-core-server.js +204 -0
  13. package/dist/core/create-core-server.js.map +1 -0
  14. package/dist/core/create-core-services.d.ts +95 -0
  15. package/dist/core/create-core-services.d.ts.map +1 -0
  16. package/dist/core/create-core-services.js +352 -0
  17. package/dist/core/create-core-services.js.map +1 -0
  18. package/dist/core-config.d.ts +119 -0
  19. package/dist/core-config.d.ts.map +1 -0
  20. package/dist/core-config.js +240 -0
  21. package/dist/core-config.js.map +1 -0
  22. package/dist/index.d.ts +36 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +36 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/modules/access/access-control.interface.d.ts +348 -0
  27. package/dist/modules/access/access-control.interface.d.ts.map +1 -0
  28. package/dist/modules/access/access-control.interface.js +2 -0
  29. package/dist/modules/access/access-control.interface.js.map +1 -0
  30. package/dist/modules/access/access-control.service.d.ts +323 -0
  31. package/dist/modules/access/access-control.service.d.ts.map +1 -0
  32. package/dist/modules/access/access-control.service.js +1431 -0
  33. package/dist/modules/access/access-control.service.js.map +1 -0
  34. package/dist/modules/access/access-errors.d.ts +34 -0
  35. package/dist/modules/access/access-errors.d.ts.map +1 -0
  36. package/dist/modules/access/access-errors.js +40 -0
  37. package/dist/modules/access/access-errors.js.map +1 -0
  38. package/dist/modules/access/access-mutation.service.d.ts +176 -0
  39. package/dist/modules/access/access-mutation.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-mutation.service.js +307 -0
  41. package/dist/modules/access/access-mutation.service.js.map +1 -0
  42. package/dist/modules/access/access-splice.d.ts +98 -0
  43. package/dist/modules/access/access-splice.d.ts.map +1 -0
  44. package/dist/modules/access/access-splice.js +346 -0
  45. package/dist/modules/access/access-splice.js.map +1 -0
  46. package/dist/modules/access/access.routes.d.ts +10 -0
  47. package/dist/modules/access/access.routes.d.ts.map +1 -0
  48. package/dist/modules/access/access.routes.js +757 -0
  49. package/dist/modules/access/access.routes.js.map +1 -0
  50. package/dist/modules/access/creator-access.d.ts +113 -0
  51. package/dist/modules/access/creator-access.d.ts.map +1 -0
  52. package/dist/modules/access/creator-access.js +212 -0
  53. package/dist/modules/access/creator-access.js.map +1 -0
  54. package/dist/modules/access/kb-read-filter.d.ts +41 -0
  55. package/dist/modules/access/kb-read-filter.d.ts.map +1 -0
  56. package/dist/modules/access/kb-read-filter.js +60 -0
  57. package/dist/modules/access/kb-read-filter.js.map +1 -0
  58. package/dist/modules/access/roles-admin.service.d.ts +240 -0
  59. package/dist/modules/access/roles-admin.service.d.ts.map +1 -0
  60. package/dist/modules/access/roles-admin.service.js +630 -0
  61. package/dist/modules/access/roles-admin.service.js.map +1 -0
  62. package/dist/modules/access/roles-edit.d.ts +91 -0
  63. package/dist/modules/access/roles-edit.d.ts.map +1 -0
  64. package/dist/modules/access/roles-edit.js +232 -0
  65. package/dist/modules/access/roles-edit.js.map +1 -0
  66. package/dist/modules/access/roles-yaml-guard.d.ts +56 -0
  67. package/dist/modules/access/roles-yaml-guard.d.ts.map +1 -0
  68. package/dist/modules/access/roles-yaml-guard.js +79 -0
  69. package/dist/modules/access/roles-yaml-guard.js.map +1 -0
  70. package/dist/modules/admin/admin-access.routes.d.ts +18 -0
  71. package/dist/modules/admin/admin-access.routes.d.ts.map +1 -0
  72. package/dist/modules/admin/admin-access.routes.js +26 -0
  73. package/dist/modules/admin/admin-access.routes.js.map +1 -0
  74. package/dist/modules/admin/admin-access.service.d.ts +24 -0
  75. package/dist/modules/admin/admin-access.service.d.ts.map +1 -0
  76. package/dist/modules/admin/admin-access.service.js +41 -0
  77. package/dist/modules/admin/admin-access.service.js.map +1 -0
  78. package/dist/modules/admin/admin.interface.d.ts +11 -0
  79. package/dist/modules/admin/admin.interface.d.ts.map +1 -0
  80. package/dist/modules/admin/admin.interface.js +2 -0
  81. package/dist/modules/admin/admin.interface.js.map +1 -0
  82. package/dist/modules/auth/account-erasure.service.d.ts +80 -0
  83. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -0
  84. package/dist/modules/auth/account-erasure.service.js +91 -0
  85. package/dist/modules/auth/account-erasure.service.js.map +1 -0
  86. package/dist/modules/auth/auth.middleware.d.ts +23 -0
  87. package/dist/modules/auth/auth.middleware.d.ts.map +1 -0
  88. package/dist/modules/auth/auth.middleware.js +68 -0
  89. package/dist/modules/auth/auth.middleware.js.map +1 -0
  90. package/dist/modules/auth/auth.routes.d.ts +25 -0
  91. package/dist/modules/auth/auth.routes.d.ts.map +1 -0
  92. package/dist/modules/auth/auth.routes.js +72 -0
  93. package/dist/modules/auth/auth.routes.js.map +1 -0
  94. package/dist/modules/auth/auth.service.d.ts +76 -0
  95. package/dist/modules/auth/auth.service.d.ts.map +1 -0
  96. package/dist/modules/auth/auth.service.js +168 -0
  97. package/dist/modules/auth/auth.service.js.map +1 -0
  98. package/dist/modules/code-mode/code-mode-names.d.ts +16 -0
  99. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -0
  100. package/dist/modules/code-mode/code-mode-names.js +30 -0
  101. package/dist/modules/code-mode/code-mode-names.js.map +1 -0
  102. package/dist/modules/code-mode/code-mode.tool.d.ts +13 -0
  103. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -0
  104. package/dist/modules/code-mode/code-mode.tool.js +112 -0
  105. package/dist/modules/code-mode/code-mode.tool.js.map +1 -0
  106. package/dist/modules/code-mode/index.d.ts +3 -0
  107. package/dist/modules/code-mode/index.d.ts.map +1 -0
  108. package/dist/modules/code-mode/index.js +3 -0
  109. package/dist/modules/code-mode/index.js.map +1 -0
  110. package/dist/modules/database/connection.d.ts +6 -0
  111. package/dist/modules/database/connection.d.ts.map +1 -0
  112. package/dist/modules/database/connection.js +12 -0
  113. package/dist/modules/database/connection.js.map +1 -0
  114. package/dist/modules/database/core-schema.d.ts +2273 -0
  115. package/dist/modules/database/core-schema.d.ts.map +1 -0
  116. package/dist/modules/database/core-schema.js +413 -0
  117. package/dist/modules/database/core-schema.js.map +1 -0
  118. package/dist/modules/database/migrate.d.ts +10 -0
  119. package/dist/modules/database/migrate.d.ts.map +1 -0
  120. package/dist/modules/database/migrate.js +40 -0
  121. package/dist/modules/database/migrate.js.map +1 -0
  122. package/dist/modules/database/schema.d.ts +12 -0
  123. package/dist/modules/database/schema.d.ts.map +1 -0
  124. package/dist/modules/database/schema.js +12 -0
  125. package/dist/modules/database/schema.js.map +1 -0
  126. package/dist/modules/diff/diff-paths.d.ts +2 -0
  127. package/dist/modules/diff/diff-paths.d.ts.map +1 -0
  128. package/dist/modules/diff/diff-paths.js +9 -0
  129. package/dist/modules/diff/diff-paths.js.map +1 -0
  130. package/dist/modules/diff/diff.config.d.ts +8 -0
  131. package/dist/modules/diff/diff.config.d.ts.map +1 -0
  132. package/dist/modules/diff/diff.config.js +13 -0
  133. package/dist/modules/diff/diff.config.js.map +1 -0
  134. package/dist/modules/diff/diff.interface.d.ts +73 -0
  135. package/dist/modules/diff/diff.interface.d.ts.map +1 -0
  136. package/dist/modules/diff/diff.interface.js +2 -0
  137. package/dist/modules/diff/diff.interface.js.map +1 -0
  138. package/dist/modules/diff/diff.routes.d.ts +35 -0
  139. package/dist/modules/diff/diff.routes.d.ts.map +1 -0
  140. package/dist/modules/diff/diff.routes.js +228 -0
  141. package/dist/modules/diff/diff.routes.js.map +1 -0
  142. package/dist/modules/diff/diff.service.d.ts +51 -0
  143. package/dist/modules/diff/diff.service.d.ts.map +1 -0
  144. package/dist/modules/diff/diff.service.js +489 -0
  145. package/dist/modules/diff/diff.service.js.map +1 -0
  146. package/dist/modules/diff/line-diff.d.ts +16 -0
  147. package/dist/modules/diff/line-diff.d.ts.map +1 -0
  148. package/dist/modules/diff/line-diff.js +67 -0
  149. package/dist/modules/diff/line-diff.js.map +1 -0
  150. package/dist/modules/mcp/mcp-auth.middleware.d.ts +27 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.js +116 -0
  153. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -0
  154. package/dist/modules/mcp/mcp-session-store.d.ts +74 -0
  155. package/dist/modules/mcp/mcp-session-store.d.ts.map +1 -0
  156. package/dist/modules/mcp/mcp-session-store.js +131 -0
  157. package/dist/modules/mcp/mcp-session-store.js.map +1 -0
  158. package/dist/modules/mcp/mcp.routes.d.ts +24 -0
  159. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -0
  160. package/dist/modules/mcp/mcp.routes.js +229 -0
  161. package/dist/modules/mcp/mcp.routes.js.map +1 -0
  162. package/dist/modules/mcp/mcp.service.d.ts +198 -0
  163. package/dist/modules/mcp/mcp.service.d.ts.map +1 -0
  164. package/dist/modules/mcp/mcp.service.js +840 -0
  165. package/dist/modules/mcp/mcp.service.js.map +1 -0
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +76 -0
  167. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -0
  168. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +291 -0
  169. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -0
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +18 -0
  171. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -0
  172. package/dist/modules/mcp/oauth/oauth-consent.routes.js +52 -0
  173. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -0
  174. package/dist/modules/mcp/oauth/oauth-state.d.ts +32 -0
  175. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -0
  176. package/dist/modules/mcp/oauth/oauth-state.js +34 -0
  177. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -0
  178. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +92 -0
  179. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -0
  180. package/dist/modules/secrets-vault/db-secrets-vault.service.js +728 -0
  181. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -0
  182. package/dist/modules/secrets-vault/index.d.ts +7 -0
  183. package/dist/modules/secrets-vault/index.d.ts.map +1 -0
  184. package/dist/modules/secrets-vault/index.js +6 -0
  185. package/dist/modules/secrets-vault/index.js.map +1 -0
  186. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts +63 -0
  187. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -0
  188. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +269 -0
  189. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -0
  190. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts +46 -0
  191. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -0
  192. package/dist/modules/secrets-vault/secrets-variable-loader.js +87 -0
  193. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -0
  194. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +210 -0
  195. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -0
  196. package/dist/modules/secrets-vault/secrets-vault.contract.js +90 -0
  197. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -0
  198. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +30 -0
  199. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -0
  200. package/dist/modules/secrets-vault/secrets-vault.routes.js +490 -0
  201. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -0
  202. package/dist/modules/skills/index.d.ts +5 -0
  203. package/dist/modules/skills/index.d.ts.map +1 -0
  204. package/dist/modules/skills/index.js +4 -0
  205. package/dist/modules/skills/index.js.map +1 -0
  206. package/dist/modules/skills/skills.contract.d.ts +58 -0
  207. package/dist/modules/skills/skills.contract.d.ts.map +1 -0
  208. package/dist/modules/skills/skills.contract.js +10 -0
  209. package/dist/modules/skills/skills.contract.js.map +1 -0
  210. package/dist/modules/skills/skills.routes.d.ts +15 -0
  211. package/dist/modules/skills/skills.routes.d.ts.map +1 -0
  212. package/dist/modules/skills/skills.routes.js +34 -0
  213. package/dist/modules/skills/skills.routes.js.map +1 -0
  214. package/dist/modules/skills/skills.service.d.ts +21 -0
  215. package/dist/modules/skills/skills.service.d.ts.map +1 -0
  216. package/dist/modules/skills/skills.service.js +202 -0
  217. package/dist/modules/skills/skills.service.js.map +1 -0
  218. package/dist/modules/skills/skills.tools.d.ts +15 -0
  219. package/dist/modules/skills/skills.tools.d.ts.map +1 -0
  220. package/dist/modules/skills/skills.tools.js +96 -0
  221. package/dist/modules/skills/skills.tools.js.map +1 -0
  222. package/dist/modules/tool-auth/external-api-key.errors.d.ts +14 -0
  223. package/dist/modules/tool-auth/external-api-key.errors.d.ts.map +1 -0
  224. package/dist/modules/tool-auth/external-api-key.errors.js +23 -0
  225. package/dist/modules/tool-auth/external-api-key.errors.js.map +1 -0
  226. package/dist/modules/tool-auth/external-api-key.interface.d.ts +90 -0
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -0
  228. package/dist/modules/tool-auth/external-api-key.interface.js +2 -0
  229. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -0
  230. package/dist/modules/tool-auth/external-api-key.service.d.ts +25 -0
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -0
  232. package/dist/modules/tool-auth/external-api-key.service.js +164 -0
  233. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -0
  234. package/dist/modules/tool-auth/internal-token.service.d.ts +62 -0
  235. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -0
  236. package/dist/modules/tool-auth/internal-token.service.js +85 -0
  237. package/dist/modules/tool-auth/internal-token.service.js.map +1 -0
  238. package/dist/modules/tool-auth/llm-usage-meter.d.ts +16 -0
  239. package/dist/modules/tool-auth/llm-usage-meter.d.ts.map +1 -0
  240. package/dist/modules/tool-auth/llm-usage-meter.js +6 -0
  241. package/dist/modules/tool-auth/llm-usage-meter.js.map +1 -0
  242. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +101 -0
  243. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -0
  244. package/dist/modules/tool-auth/tool-auth.middleware.js +166 -0
  245. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -0
  246. package/dist/modules/tool-helpers/index.d.ts +19 -0
  247. package/dist/modules/tool-helpers/index.d.ts.map +1 -0
  248. package/dist/modules/tool-helpers/index.js +18 -0
  249. package/dist/modules/tool-helpers/index.js.map +1 -0
  250. package/dist/modules/tool-helpers/tool-context.d.ts +35 -0
  251. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -0
  252. package/dist/modules/tool-helpers/tool-context.js +66 -0
  253. package/dist/modules/tool-helpers/tool-context.js.map +1 -0
  254. package/dist/modules/tool-helpers/tool-def.d.ts +33 -0
  255. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -0
  256. package/dist/modules/tool-helpers/tool-def.js +57 -0
  257. package/dist/modules/tool-helpers/tool-def.js.map +1 -0
  258. package/dist/modules/tool-helpers/tool-handler.d.ts +19 -0
  259. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -0
  260. package/dist/modules/tool-helpers/tool-handler.js +84 -0
  261. package/dist/modules/tool-helpers/tool-handler.js.map +1 -0
  262. package/dist/modules/tool-helpers/tool.contract.d.ts +64 -0
  263. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -0
  264. package/dist/modules/tool-helpers/tool.contract.js +24 -0
  265. package/dist/modules/tool-helpers/tool.contract.js.map +1 -0
  266. package/dist/modules/tool-helpers/validate-token.d.ts +31 -0
  267. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -0
  268. package/dist/modules/tool-helpers/validate-token.js +20 -0
  269. package/dist/modules/tool-helpers/validate-token.js.map +1 -0
  270. package/dist/modules/tool-manuals/index.d.ts +5 -0
  271. package/dist/modules/tool-manuals/index.d.ts.map +1 -0
  272. package/dist/modules/tool-manuals/index.js +5 -0
  273. package/dist/modules/tool-manuals/index.js.map +1 -0
  274. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +182 -0
  275. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -0
  276. package/dist/modules/tool-manuals/tool-manuals.contract.js +19 -0
  277. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -0
  278. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +24 -0
  279. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -0
  280. package/dist/modules/tool-manuals/tool-manuals.routes.js +102 -0
  281. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -0
  282. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +108 -0
  283. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -0
  284. package/dist/modules/tool-manuals/tool-manuals.service.js +710 -0
  285. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -0
  286. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +35 -0
  287. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -0
  288. package/dist/modules/tool-manuals/tool-manuals.tools.js +157 -0
  289. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -0
  290. package/dist/modules/tool-registry/manual.routes.d.ts +6 -0
  291. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -0
  292. package/dist/modules/tool-registry/manual.routes.js +76 -0
  293. package/dist/modules/tool-registry/manual.routes.js.map +1 -0
  294. package/dist/modules/tool-registry/tool-registry.d.ts +23 -0
  295. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -0
  296. package/dist/modules/tool-registry/tool-registry.js +46 -0
  297. package/dist/modules/tool-registry/tool-registry.js.map +1 -0
  298. package/dist/modules/tool-registry/tool.contract.d.ts +48 -0
  299. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -0
  300. package/dist/modules/tool-registry/tool.contract.js +2 -0
  301. package/dist/modules/tool-registry/tool.contract.js.map +1 -0
  302. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +14 -0
  303. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -0
  304. package/dist/modules/workflow/agent-tools/workflow.tools.js +437 -0
  305. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -0
  306. package/dist/modules/workflow/event-bus.d.ts +107 -0
  307. package/dist/modules/workflow/event-bus.d.ts.map +1 -0
  308. package/dist/modules/workflow/event-bus.js +224 -0
  309. package/dist/modules/workflow/event-bus.js.map +1 -0
  310. package/dist/modules/workflow/events.routes.d.ts +80 -0
  311. package/dist/modules/workflow/events.routes.d.ts.map +1 -0
  312. package/dist/modules/workflow/events.routes.js +216 -0
  313. package/dist/modules/workflow/events.routes.js.map +1 -0
  314. package/dist/modules/workflow/file-change-notifier.d.ts +38 -0
  315. package/dist/modules/workflow/file-change-notifier.d.ts.map +1 -0
  316. package/dist/modules/workflow/file-change-notifier.js +22 -0
  317. package/dist/modules/workflow/file-change-notifier.js.map +1 -0
  318. package/dist/modules/workflow/file-lock.service.d.ts +57 -0
  319. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -0
  320. package/dist/modules/workflow/file-lock.service.js +202 -0
  321. package/dist/modules/workflow/file-lock.service.js.map +1 -0
  322. package/dist/modules/workflow/git/branch-name.d.ts +10 -0
  323. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -0
  324. package/dist/modules/workflow/git/branch-name.js +76 -0
  325. package/dist/modules/workflow/git/branch-name.js.map +1 -0
  326. package/dist/modules/workflow/git/clone-config.d.ts +61 -0
  327. package/dist/modules/workflow/git/clone-config.d.ts.map +1 -0
  328. package/dist/modules/workflow/git/clone-config.js +69 -0
  329. package/dist/modules/workflow/git/clone-config.js.map +1 -0
  330. package/dist/modules/workflow/git/git-version.d.ts +34 -0
  331. package/dist/modules/workflow/git/git-version.d.ts.map +1 -0
  332. package/dist/modules/workflow/git/git-version.js +63 -0
  333. package/dist/modules/workflow/git/git-version.js.map +1 -0
  334. package/dist/modules/workflow/git/git.service.d.ts +427 -0
  335. package/dist/modules/workflow/git/git.service.d.ts.map +1 -0
  336. package/dist/modules/workflow/git/git.service.js +1987 -0
  337. package/dist/modules/workflow/git/git.service.js.map +1 -0
  338. package/dist/modules/workflow/git/mutex.d.ts +11 -0
  339. package/dist/modules/workflow/git/mutex.d.ts.map +1 -0
  340. package/dist/modules/workflow/git/mutex.js +23 -0
  341. package/dist/modules/workflow/git/mutex.js.map +1 -0
  342. package/dist/modules/workflow/git/pull-request.service.d.ts +124 -0
  343. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -0
  344. package/dist/modules/workflow/git/pull-request.service.js +328 -0
  345. package/dist/modules/workflow/git/pull-request.service.js.map +1 -0
  346. package/dist/modules/workflow/locking-filesystem.d.ts +133 -0
  347. package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -0
  348. package/dist/modules/workflow/locking-filesystem.js +400 -0
  349. package/dist/modules/workflow/locking-filesystem.js.map +1 -0
  350. package/dist/modules/workflow/pending-commits.service.d.ts +221 -0
  351. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -0
  352. package/dist/modules/workflow/pending-commits.service.js +364 -0
  353. package/dist/modules/workflow/pending-commits.service.js.map +1 -0
  354. package/dist/modules/workflow/pending-commits.worker.d.ts +148 -0
  355. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -0
  356. package/dist/modules/workflow/pending-commits.worker.js +245 -0
  357. package/dist/modules/workflow/pending-commits.worker.js.map +1 -0
  358. package/dist/modules/workflow/read-only-filesystem.d.ts +24 -0
  359. package/dist/modules/workflow/read-only-filesystem.d.ts.map +1 -0
  360. package/dist/modules/workflow/read-only-filesystem.js +39 -0
  361. package/dist/modules/workflow/read-only-filesystem.js.map +1 -0
  362. package/dist/modules/workflow/recovery-bot.d.ts +31 -0
  363. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -0
  364. package/dist/modules/workflow/recovery-bot.js +64 -0
  365. package/dist/modules/workflow/recovery-bot.js.map +1 -0
  366. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +137 -0
  367. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -0
  368. package/dist/modules/workflow/review-workflow/review-workflow.interface.js +2 -0
  369. package/dist/modules/workflow/review-workflow/review-workflow.interface.js.map +1 -0
  370. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +26 -0
  371. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -0
  372. package/dist/modules/workflow/review-workflow/review-workflow.service.js +636 -0
  373. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -0
  374. package/dist/modules/workflow/sanitize-error.d.ts +29 -0
  375. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -0
  376. package/dist/modules/workflow/sanitize-error.js +60 -0
  377. package/dist/modules/workflow/sanitize-error.js.map +1 -0
  378. package/dist/modules/workflow/session-ontology.policy.d.ts +52 -0
  379. package/dist/modules/workflow/session-ontology.policy.d.ts.map +1 -0
  380. package/dist/modules/workflow/session-ontology.policy.js +62 -0
  381. package/dist/modules/workflow/session-ontology.policy.js.map +1 -0
  382. package/dist/modules/workflow/session-ontology.service.d.ts +100 -0
  383. package/dist/modules/workflow/session-ontology.service.d.ts.map +1 -0
  384. package/dist/modules/workflow/session-ontology.service.js +143 -0
  385. package/dist/modules/workflow/session-ontology.service.js.map +1 -0
  386. package/dist/modules/workflow/workflow-hooks.d.ts +87 -0
  387. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -0
  388. package/dist/modules/workflow/workflow-hooks.js +35 -0
  389. package/dist/modules/workflow/workflow-hooks.js.map +1 -0
  390. package/dist/modules/workflow/workflow.errors.d.ts +171 -0
  391. package/dist/modules/workflow/workflow.errors.d.ts.map +1 -0
  392. package/dist/modules/workflow/workflow.errors.js +260 -0
  393. package/dist/modules/workflow/workflow.errors.js.map +1 -0
  394. package/dist/modules/workflow/workflow.routes.d.ts +26 -0
  395. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -0
  396. package/dist/modules/workflow/workflow.routes.js +804 -0
  397. package/dist/modules/workflow/workflow.routes.js.map +1 -0
  398. package/dist/modules/workflow/workflow.service.d.ts +385 -0
  399. package/dist/modules/workflow/workflow.service.d.ts.map +1 -0
  400. package/dist/modules/workflow/workflow.service.js +1155 -0
  401. package/dist/modules/workflow/workflow.service.js.map +1 -0
  402. package/dist/modules/workspace/bevel-ignore.d.ts +25 -0
  403. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -0
  404. package/dist/modules/workspace/bevel-ignore.js +57 -0
  405. package/dist/modules/workspace/bevel-ignore.js.map +1 -0
  406. package/dist/modules/workspace/kb-seed.interface.d.ts +36 -0
  407. package/dist/modules/workspace/kb-seed.interface.d.ts.map +1 -0
  408. package/dist/modules/workspace/kb-seed.interface.js +2 -0
  409. package/dist/modules/workspace/kb-seed.interface.js.map +1 -0
  410. package/dist/modules/workspace/kb-seed.service.d.ts +70 -0
  411. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -0
  412. package/dist/modules/workspace/kb-seed.service.js +325 -0
  413. package/dist/modules/workspace/kb-seed.service.js.map +1 -0
  414. package/dist/modules/workspace/routine-write-policy.d.ts +49 -0
  415. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -0
  416. package/dist/modules/workspace/routine-write-policy.js +46 -0
  417. package/dist/modules/workspace/routine-write-policy.js.map +1 -0
  418. package/dist/modules/workspace/session-ontology.gate.d.ts +112 -0
  419. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -0
  420. package/dist/modules/workspace/session-ontology.gate.js +161 -0
  421. package/dist/modules/workspace/session-ontology.gate.js.map +1 -0
  422. package/dist/modules/workspace/session-sink.d.ts +25 -0
  423. package/dist/modules/workspace/session-sink.d.ts.map +1 -0
  424. package/dist/modules/workspace/session-sink.js +8 -0
  425. package/dist/modules/workspace/session-sink.js.map +1 -0
  426. package/dist/modules/workspace/spill-store.d.ts +24 -0
  427. package/dist/modules/workspace/spill-store.d.ts.map +1 -0
  428. package/dist/modules/workspace/spill-store.js +87 -0
  429. package/dist/modules/workspace/spill-store.js.map +1 -0
  430. package/dist/modules/workspace/workspace.routes.d.ts +25 -0
  431. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -0
  432. package/dist/modules/workspace/workspace.routes.js +1046 -0
  433. package/dist/modules/workspace/workspace.routes.js.map +1 -0
  434. package/dist/modules/workspace/workspace.service.d.ts +339 -0
  435. package/dist/modules/workspace/workspace.service.d.ts.map +1 -0
  436. package/dist/modules/workspace/workspace.service.js +1221 -0
  437. package/dist/modules/workspace/workspace.service.js.map +1 -0
  438. package/dist/modules/workspace/workspace.tools.d.ts +19 -0
  439. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -0
  440. package/dist/modules/workspace/workspace.tools.js +767 -0
  441. package/dist/modules/workspace/workspace.tools.js.map +1 -0
  442. package/dist/shared/frontmatter-id.d.ts +35 -0
  443. package/dist/shared/frontmatter-id.d.ts.map +1 -0
  444. package/dist/shared/frontmatter-id.js +68 -0
  445. package/dist/shared/frontmatter-id.js.map +1 -0
  446. package/dist/shared/fs-walk.d.ts +9 -0
  447. package/dist/shared/fs-walk.d.ts.map +1 -0
  448. package/dist/shared/fs-walk.js +34 -0
  449. package/dist/shared/fs-walk.js.map +1 -0
  450. package/dist/shared/hash-email.d.ts +11 -0
  451. package/dist/shared/hash-email.d.ts.map +1 -0
  452. package/dist/shared/hash-email.js +14 -0
  453. package/dist/shared/hash-email.js.map +1 -0
  454. package/dist/shared/kb-layout.d.ts +38 -0
  455. package/dist/shared/kb-layout.d.ts.map +1 -0
  456. package/dist/shared/kb-layout.js +102 -0
  457. package/dist/shared/kb-layout.js.map +1 -0
  458. package/dist/shared/kb-layout.test.d.ts +2 -0
  459. package/dist/shared/kb-layout.test.d.ts.map +1 -0
  460. package/dist/shared/kb-layout.test.js +67 -0
  461. package/dist/shared/kb-layout.test.js.map +1 -0
  462. package/dist/shared/ssrf.d.ts +20 -0
  463. package/dist/shared/ssrf.d.ts.map +1 -0
  464. package/dist/shared/ssrf.js +89 -0
  465. package/dist/shared/ssrf.js.map +1 -0
  466. package/dist/shared/token-crypto.d.ts +16 -0
  467. package/dist/shared/token-crypto.d.ts.map +1 -0
  468. package/dist/shared/token-crypto.js +49 -0
  469. package/dist/shared/token-crypto.js.map +1 -0
  470. package/dist/shared/ttl-cache.d.ts +16 -0
  471. package/dist/shared/ttl-cache.d.ts.map +1 -0
  472. package/dist/shared/ttl-cache.js +27 -0
  473. package/dist/shared/ttl-cache.js.map +1 -0
  474. package/dist/shared/utcp-namespace.d.ts +46 -0
  475. package/dist/shared/utcp-namespace.d.ts.map +1 -0
  476. package/dist/shared/utcp-namespace.js +82 -0
  477. package/dist/shared/utcp-namespace.js.map +1 -0
  478. package/dist/version.d.ts +17 -0
  479. package/dist/version.d.ts.map +1 -0
  480. package/dist/version.js +19 -0
  481. package/dist/version.js.map +1 -0
  482. package/kb-template/.bevelignore +21 -0
  483. package/kb-template/Agents/.gitkeep +0 -0
  484. package/kb-template/Agents/README.md +38 -0
  485. package/kb-template/CLAUDE.md +419 -0
  486. package/kb-template/Data/.gitkeep +0 -0
  487. package/kb-template/Data/README.md +39 -0
  488. package/kb-template/KnowledgeBase/.gitkeep +0 -0
  489. package/kb-template/KnowledgeBase/README.md +37 -0
  490. package/kb-template/Pipelines/.gitkeep +0 -0
  491. package/kb-template/Pipelines/README.md +38 -0
  492. package/kb-template/Skills/.gitkeep +0 -0
  493. package/kb-template/Skills/README.md +32 -0
  494. package/kb-template/Tools/.gitkeep +0 -0
  495. package/kb-template/Tools/README.md +42 -0
  496. package/kb-template/access.md +36 -0
  497. package/migrations/0000_core_init.sql +243 -0
  498. package/migrations/meta/0000_snapshot.json +1480 -0
  499. package/migrations/meta/_journal.json +13 -0
  500. package/package.json +80 -0
  501. package/src/assets.ts +25 -0
  502. package/src/core/core-ports.ts +91 -0
  503. package/src/core/create-core-server.ts +338 -0
  504. package/src/core/create-core-services.ts +501 -0
  505. package/src/core-config.ts +257 -0
  506. package/src/index.ts +69 -0
  507. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -0
  508. package/src/modules/access/__tests__/access-control.read-batch.test.ts +132 -0
  509. package/src/modules/access/__tests__/access-control.service.test.ts +1011 -0
  510. package/src/modules/access/__tests__/access-mutation.service.test.ts +439 -0
  511. package/src/modules/access/__tests__/access-splice.test.ts +247 -0
  512. package/src/modules/access/__tests__/access.routes.revoke.test.ts +407 -0
  513. package/src/modules/access/__tests__/creator-access.test.ts +217 -0
  514. package/src/modules/access/__tests__/grant-sources.test.ts +204 -0
  515. package/src/modules/access/__tests__/kb-read-filter.test.ts +70 -0
  516. package/src/modules/access/__tests__/roles-admin.service.test.ts +469 -0
  517. package/src/modules/access/__tests__/roles-edit.test.ts +138 -0
  518. package/src/modules/access/__tests__/roles-yaml-guard.test.ts +70 -0
  519. package/src/modules/access/__tests__/roles.routes.test.ts +234 -0
  520. package/src/modules/access/access-control.interface.ts +383 -0
  521. package/src/modules/access/access-control.service.ts +1794 -0
  522. package/src/modules/access/access-errors.ts +60 -0
  523. package/src/modules/access/access-mutation.service.ts +369 -0
  524. package/src/modules/access/access-splice.ts +413 -0
  525. package/src/modules/access/access.routes.ts +894 -0
  526. package/src/modules/access/creator-access.ts +270 -0
  527. package/src/modules/access/kb-read-filter.ts +76 -0
  528. package/src/modules/access/roles-admin.service.ts +741 -0
  529. package/src/modules/access/roles-edit.ts +248 -0
  530. package/src/modules/access/roles-yaml-guard.ts +89 -0
  531. package/src/modules/admin/admin-access.routes.ts +29 -0
  532. package/src/modules/admin/admin-access.service.ts +38 -0
  533. package/src/modules/admin/admin.interface.ts +10 -0
  534. package/src/modules/auth/account-erasure.service.ts +183 -0
  535. package/src/modules/auth/auth.middleware.ts +78 -0
  536. package/src/modules/auth/auth.routes.ts +97 -0
  537. package/src/modules/auth/auth.service.ts +199 -0
  538. package/src/modules/code-mode/code-mode-names.ts +38 -0
  539. package/src/modules/code-mode/code-mode.tool.ts +117 -0
  540. package/src/modules/code-mode/index.ts +6 -0
  541. package/src/modules/database/connection.ts +15 -0
  542. package/src/modules/database/core-schema.ts +431 -0
  543. package/src/modules/database/migrate.ts +43 -0
  544. package/src/modules/database/schema.ts +11 -0
  545. package/src/modules/diff/__tests__/diff.routes.read-gate.test.ts +87 -0
  546. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -0
  547. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +184 -0
  548. package/src/modules/diff/__tests__/diff.service.test.ts +202 -0
  549. package/src/modules/diff/diff-paths.ts +9 -0
  550. package/src/modules/diff/diff.config.ts +12 -0
  551. package/src/modules/diff/diff.interface.ts +78 -0
  552. package/src/modules/diff/diff.routes.ts +277 -0
  553. package/src/modules/diff/diff.service.ts +508 -0
  554. package/src/modules/diff/line-diff.ts +79 -0
  555. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +287 -0
  556. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +249 -0
  557. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +253 -0
  558. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +151 -0
  559. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +150 -0
  560. package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +92 -0
  561. package/src/modules/mcp/__tests__/mcp.service.test.ts +414 -0
  562. package/src/modules/mcp/mcp-auth.middleware.ts +126 -0
  563. package/src/modules/mcp/mcp-session-store.ts +162 -0
  564. package/src/modules/mcp/mcp.routes.ts +263 -0
  565. package/src/modules/mcp/mcp.service.ts +973 -0
  566. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +401 -0
  567. package/src/modules/mcp/oauth/oauth-consent.routes.ts +60 -0
  568. package/src/modules/mcp/oauth/oauth-state.ts +61 -0
  569. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +277 -0
  570. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +206 -0
  571. package/src/modules/secrets-vault/__tests__/mcp-oauth-discovery.service.test.ts +178 -0
  572. package/src/modules/secrets-vault/__tests__/scopes-covered.test.ts +101 -0
  573. package/src/modules/secrets-vault/__tests__/secrets-variable-loader.test.ts +54 -0
  574. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -0
  575. package/src/modules/secrets-vault/db-secrets-vault.service.ts +792 -0
  576. package/src/modules/secrets-vault/index.ts +25 -0
  577. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +289 -0
  578. package/src/modules/secrets-vault/secrets-variable-loader.ts +104 -0
  579. package/src/modules/secrets-vault/secrets-vault.contract.ts +270 -0
  580. package/src/modules/secrets-vault/secrets-vault.routes.ts +526 -0
  581. package/src/modules/skills/__tests__/skills.service.test.ts +176 -0
  582. package/src/modules/skills/index.ts +10 -0
  583. package/src/modules/skills/skills.contract.ts +54 -0
  584. package/src/modules/skills/skills.routes.ts +38 -0
  585. package/src/modules/skills/skills.service.ts +245 -0
  586. package/src/modules/skills/skills.tools.ts +121 -0
  587. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +358 -0
  588. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +79 -0
  589. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +88 -0
  590. package/src/modules/tool-auth/external-api-key.errors.ts +25 -0
  591. package/src/modules/tool-auth/external-api-key.interface.ts +99 -0
  592. package/src/modules/tool-auth/external-api-key.service.ts +209 -0
  593. package/src/modules/tool-auth/internal-token.service.ts +124 -0
  594. package/src/modules/tool-auth/llm-usage-meter.ts +19 -0
  595. package/src/modules/tool-auth/tool-auth.middleware.ts +220 -0
  596. package/src/modules/tool-helpers/__tests__/phase0-loopback-spike.test.ts +129 -0
  597. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +95 -0
  598. package/src/modules/tool-helpers/__tests__/tools-surface.test.ts +146 -0
  599. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +75 -0
  600. package/src/modules/tool-helpers/index.ts +18 -0
  601. package/src/modules/tool-helpers/tool-context.ts +98 -0
  602. package/src/modules/tool-helpers/tool-def.ts +73 -0
  603. package/src/modules/tool-helpers/tool-handler.ts +91 -0
  604. package/src/modules/tool-helpers/tool.contract.ts +85 -0
  605. package/src/modules/tool-helpers/validate-token.ts +50 -0
  606. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +200 -0
  607. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +499 -0
  608. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -0
  609. package/src/modules/tool-manuals/index.ts +12 -0
  610. package/src/modules/tool-manuals/tool-manuals.contract.ts +183 -0
  611. package/src/modules/tool-manuals/tool-manuals.routes.ts +110 -0
  612. package/src/modules/tool-manuals/tool-manuals.service.ts +781 -0
  613. package/src/modules/tool-manuals/tool-manuals.tools.ts +202 -0
  614. package/src/modules/tool-registry/__tests__/tool-registry.test.ts +64 -0
  615. package/src/modules/tool-registry/manual.routes.ts +87 -0
  616. package/src/modules/tool-registry/tool-registry.ts +55 -0
  617. package/src/modules/tool-registry/tool.contract.ts +52 -0
  618. package/src/modules/workflow/__tests__/event-bus.test.ts +322 -0
  619. package/src/modules/workflow/__tests__/file-change-notifier.test.ts +51 -0
  620. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +67 -0
  621. package/src/modules/workflow/__tests__/locking-filesystem.test.ts +445 -0
  622. package/src/modules/workflow/__tests__/pending-commits.canonical.test.ts +36 -0
  623. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +243 -0
  624. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +308 -0
  625. package/src/modules/workflow/__tests__/sanitize-error.test.ts +67 -0
  626. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +62 -0
  627. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +200 -0
  628. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +232 -0
  629. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +380 -0
  630. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +340 -0
  631. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +192 -0
  632. package/src/modules/workflow/agent-tools/workflow.tools.ts +502 -0
  633. package/src/modules/workflow/event-bus.ts +262 -0
  634. package/src/modules/workflow/events.routes.ts +266 -0
  635. package/src/modules/workflow/file-change-notifier.ts +54 -0
  636. package/src/modules/workflow/file-lock.service.ts +269 -0
  637. package/src/modules/workflow/git/__tests__/branch-name.test.ts +86 -0
  638. package/src/modules/workflow/git/__tests__/branchAuthor.test.ts +80 -0
  639. package/src/modules/workflow/git/__tests__/clone-config.test.ts +15 -0
  640. package/src/modules/workflow/git/__tests__/git-test-helpers.ts +47 -0
  641. package/src/modules/workflow/git/__tests__/git-version.failure.test.ts +24 -0
  642. package/src/modules/workflow/git/__tests__/git-version.test.ts +49 -0
  643. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +389 -0
  644. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  645. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +105 -0
  646. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +211 -0
  647. package/src/modules/workflow/git/__tests__/git.service.createBranch.test.ts +154 -0
  648. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +320 -0
  649. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +202 -0
  650. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +453 -0
  651. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +198 -0
  652. package/src/modules/workflow/git/__tests__/git.service.listTrackedFiles.test.ts +125 -0
  653. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +219 -0
  654. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +145 -0
  655. package/src/modules/workflow/git/__tests__/git.service.pendingChanges.test.ts +240 -0
  656. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +272 -0
  657. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +232 -0
  658. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +101 -0
  659. package/src/modules/workflow/git/__tests__/git.service.status.test.ts +127 -0
  660. package/src/modules/workflow/git/__tests__/mutex.test.ts +78 -0
  661. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +225 -0
  662. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +101 -0
  663. package/src/modules/workflow/git/branch-name.ts +70 -0
  664. package/src/modules/workflow/git/clone-config.ts +71 -0
  665. package/src/modules/workflow/git/git-version.ts +70 -0
  666. package/src/modules/workflow/git/git.service.ts +2248 -0
  667. package/src/modules/workflow/git/mutex.ts +22 -0
  668. package/src/modules/workflow/git/pull-request.service.ts +447 -0
  669. package/src/modules/workflow/locking-filesystem.ts +489 -0
  670. package/src/modules/workflow/pending-commits.service.ts +444 -0
  671. package/src/modules/workflow/pending-commits.worker.ts +361 -0
  672. package/src/modules/workflow/read-only-filesystem.ts +54 -0
  673. package/src/modules/workflow/recovery-bot.ts +73 -0
  674. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +560 -0
  675. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +176 -0
  676. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +258 -0
  677. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +199 -0
  678. package/src/modules/workflow/review-workflow/review-workflow.service.ts +806 -0
  679. package/src/modules/workflow/sanitize-error.ts +62 -0
  680. package/src/modules/workflow/session-ontology.policy.ts +70 -0
  681. package/src/modules/workflow/session-ontology.service.ts +178 -0
  682. package/src/modules/workflow/workflow-hooks.ts +101 -0
  683. package/src/modules/workflow/workflow.errors.ts +285 -0
  684. package/src/modules/workflow/workflow.routes.ts +858 -0
  685. package/src/modules/workflow/workflow.service.ts +1555 -0
  686. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +213 -0
  687. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +68 -0
  688. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -0
  689. package/src/modules/workspace/__tests__/spill-store.test.ts +43 -0
  690. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +233 -0
  691. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +176 -0
  692. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +303 -0
  693. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +276 -0
  694. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +120 -0
  695. package/src/modules/workspace/__tests__/workspace.service.test.ts +565 -0
  696. package/src/modules/workspace/__tests__/workspace.tools.test.ts +504 -0
  697. package/src/modules/workspace/bevel-ignore.ts +62 -0
  698. package/src/modules/workspace/kb-seed.interface.ts +36 -0
  699. package/src/modules/workspace/kb-seed.service.ts +361 -0
  700. package/src/modules/workspace/routine-write-policy.ts +79 -0
  701. package/src/modules/workspace/session-ontology.gate.ts +189 -0
  702. package/src/modules/workspace/session-sink.ts +25 -0
  703. package/src/modules/workspace/spill-store.ts +87 -0
  704. package/src/modules/workspace/workspace.routes.ts +1114 -0
  705. package/src/modules/workspace/workspace.service.ts +1343 -0
  706. package/src/modules/workspace/workspace.tools.ts +885 -0
  707. package/src/shared/__tests__/frontmatter-id.test.ts +88 -0
  708. package/src/shared/__tests__/fs-walk.test.ts +42 -0
  709. package/src/shared/__tests__/ssrf.test.ts +53 -0
  710. package/src/shared/__tests__/utcp-namespace.test.ts +19 -0
  711. package/src/shared/frontmatter-id.ts +72 -0
  712. package/src/shared/fs-walk.ts +30 -0
  713. package/src/shared/hash-email.ts +14 -0
  714. package/src/shared/kb-layout.test.ts +88 -0
  715. package/src/shared/kb-layout.ts +101 -0
  716. package/src/shared/ssrf.ts +78 -0
  717. package/src/shared/token-crypto.ts +54 -0
  718. package/src/shared/ttl-cache.ts +27 -0
  719. package/src/shared/utcp-namespace.ts +88 -0
  720. package/src/version.ts +19 -0
@@ -0,0 +1,1221 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import AdmZip from 'adm-zip';
6
+ import { assertValidRelativePath, validateFilename, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
7
+ import { BevelIgnoreStack } from './bevel-ignore.js';
8
+ import { assertValidBranchName } from '../workflow/git/branch-name.js';
9
+ import { cloneTrackingConfigArgs, SAFE_IMPLICIT_FETCH_ARGS } from '../workflow/git/clone-config.js';
10
+ // Zip-bomb guardrails. The upload cap is 50 MB compressed; even a generous
11
+ // 100× expansion ratio still fits inside these limits, so a well-behaved
12
+ // archive will never trip them. They exist so a hostile or malformed zip
13
+ // can't run the container out of disk by inflating to gigabytes.
14
+ const UNZIP_MAX_ENTRIES = 10_000;
15
+ const UNZIP_MAX_ENTRY_BYTES = 100 * 1024 * 1024; // 100 MB per file
16
+ const UNZIP_MAX_TOTAL_BYTES = 500 * 1024 * 1024; // 500 MB across the whole archive
17
+ // Folder-download cap. The whole zip is built in memory by adm-zip (no
18
+ // streaming) so the cap also bounds peak heap usage for one download.
19
+ // Reuses the unzip total as a single "fits in a workspace" budget.
20
+ const ZIP_DOWNLOAD_MAX_BYTES = UNZIP_MAX_TOTAL_BYTES;
21
+ /**
22
+ * Thrown by `createFolderZip` when the cumulative uncompressed bytes exceed
23
+ * `ZIP_DOWNLOAD_MAX_BYTES`. Carries a 413-ready message; the route layer
24
+ * checks `instanceof` and maps it accordingly.
25
+ */
26
+ export class FolderTooLargeError extends Error {
27
+ limitBytes;
28
+ constructor(limitBytes) {
29
+ super(`Folder exceeds the ${limitBytes}-byte download size limit`);
30
+ this.limitBytes = limitBytes;
31
+ this.name = 'FolderTooLargeError';
32
+ }
33
+ }
34
+ const execFileAsync = promisify(execFile);
35
+ /**
36
+ * Identity stamped on the per-branch clone's git config. Every workflow
37
+ * commit overrides this with `--author=…` so the human shows up in
38
+ * `git log`; this is just the fallback committer identity so plain
39
+ * git operations have something to attribute to.
40
+ */
41
+ const BOT_NAME = 'Bevel Workflow';
42
+ const BOT_EMAIL = 'bevel-workflow@bevel.software';
43
+ /** Redact any token that might leak into an error message. */
44
+ function redactError(err) {
45
+ const token = process.env.GITHUB_TOKEN;
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ return token ? msg.replaceAll(token, '***') : msg;
48
+ }
49
+ const FETCH_CACHE_TTL_MS = 30_000;
50
+ /**
51
+ * Map a branch name to the directory that contains its workspace clone.
52
+ * `encodeURIComponent` handles `/` in branch names (e.g. `alice/foo`) by
53
+ * encoding it to `%2F` — single-segment-safe on every supported filesystem.
54
+ */
55
+ export function workspaceIdForBranch(branch) {
56
+ return encodeURIComponent(branch);
57
+ }
58
+ export function branchForWorkspaceId(workspaceId) {
59
+ return decodeURIComponent(workspaceId);
60
+ }
61
+ export class WorkspaceService {
62
+ workspacesRoot;
63
+ kbRepoUrl;
64
+ kbDirName;
65
+ gitUsername;
66
+ /** Maps branch → absolute directory path. Lazily populated. */
67
+ branchDirs = new Map();
68
+ /**
69
+ * In-flight bootstrap promises keyed by branch — concurrent callers for
70
+ * the same branch share a single clone instead of racing to overwrite
71
+ * each other's working tree.
72
+ */
73
+ inFlightBootstraps = new Map();
74
+ /** Per-workspace-dir `git fetch origin` timestamp + in-flight tracker. */
75
+ lastFetchAt = new Map();
76
+ inFlightFetches = new Map();
77
+ /**
78
+ * Late-bound by the composition root — DiffService depends on
79
+ * WorkspaceService for path resolution.
80
+ */
81
+ diffService = null;
82
+ /**
83
+ * Late-bound by the composition root. Awaited once before the first per-branch
84
+ * clone so the KB remote is guaranteed to carry the protected branches + base
85
+ * scaffolding — a fresh (or partially-populated) remote is seeded rather than
86
+ * assumed. Null in tests / setups that pre-provision the remote themselves.
87
+ */
88
+ seedService = null;
89
+ /**
90
+ * Late-bound by the composition root: notified with the workspace id
91
+ * right after a branch's clone is created. The git layer uses this to
92
+ * skip the otherwise-redundant implicit `git fetch` on the first
93
+ * `listBranches` — a fresh clone has already downloaded every ref.
94
+ */
95
+ onWorkspaceCloned = null;
96
+ constructor(workspacesRoot, kbRepoUrl, kbDirName, gitUsername = 'x-access-token') {
97
+ this.workspacesRoot = workspacesRoot;
98
+ this.kbRepoUrl = kbRepoUrl;
99
+ this.kbDirName = kbDirName;
100
+ this.gitUsername = gitUsername;
101
+ }
102
+ setDiffService(diffService) {
103
+ this.diffService = diffService;
104
+ }
105
+ setSeedService(seedService) {
106
+ this.seedService = seedService;
107
+ }
108
+ setWorkspaceClonedListener(listener) {
109
+ this.onWorkspaceCloned = listener;
110
+ }
111
+ /** Build git clone args that inject credentials via -c headers so the token never appears in the repo URL. */
112
+ gitCloneArgs(targetDir, branch, referenceRepo) {
113
+ const token = process.env.GITHUB_TOKEN;
114
+ // core.longpaths=true: KB has paths >260 chars; without this the clone silently drops those files on Windows.
115
+ const args = ['clone', '-c', 'core.longpaths=true', '-b', branch];
116
+ // Borrow the object database of an existing sibling clone. Every commit
117
+ // in the KB is already on disk locally, so object transfer drops to
118
+ // ~zero and a cold branch switch pays only the ref-negotiation
119
+ // round-trip instead of a full download. `--dissociate` copies the
120
+ // borrowed objects into the new repo and drops the alternates link, so
121
+ // the new clone stays independent even if the sibling is later swept
122
+ // or gc'd. Still a real clone of the real remote — all `refs/remotes/
123
+ // origin/*` are populated as usual.
124
+ if (referenceRepo) {
125
+ args.push('--reference', referenceRepo, '--dissociate');
126
+ }
127
+ if (token) {
128
+ // The helper script reads from GITHUB_TOKEN at runtime — the token value never appears in args.
129
+ // Username is provider-specific (GitHub `x-access-token`, GitLab `oauth2`, …); the token is
130
+ // always the Basic-auth password, which every major host accepts.
131
+ args.push('-c', `credential.helper=!f() { echo "username=${this.gitUsername}"; echo "password=$GITHUB_TOKEN"; }; f`);
132
+ }
133
+ args.push(this.kbRepoUrl, targetDir);
134
+ return args;
135
+ }
136
+ /**
137
+ * Find an existing workspace clone on disk to use as a `--reference` for
138
+ * a new clone. Any clone of the KB works — they all share the same object
139
+ * history. Returns the absolute path to the inner repo dir, or null when
140
+ * no sibling exists (the first branch after a cold start). Scans disk
141
+ * rather than the in-memory `branchDirs` map so clones that survived a
142
+ * process restart are still found.
143
+ */
144
+ async findSiblingRepo(excludeBranch) {
145
+ const excludeId = workspaceIdForBranch(excludeBranch);
146
+ let entries;
147
+ try {
148
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ for (const entry of entries) {
154
+ if (!entry.isDirectory() || entry.name === excludeId)
155
+ continue;
156
+ const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
157
+ try {
158
+ await fs.access(path.join(repoDir, '.git'));
159
+ return repoDir;
160
+ }
161
+ catch {
162
+ // No `.git` — half-built or unrelated dir; keep looking.
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ /**
168
+ * Return the id of any workspace already cloned on disk, or null if none
169
+ * exists. Every clone tracks the same origin, so for a *repo-global* operation
170
+ * that doesn't act on a particular draft — listing branches, reading or
171
+ * merging a change request by number — any existing clone is interchangeable.
172
+ * Reusing one that's already on disk avoids cloning a branch (or failing when
173
+ * a conventional default branch isn't on the remote) just to run a global op.
174
+ * Scans disk rather than the in-memory `branchDirs` map so clones that
175
+ * survived a process restart are still found.
176
+ */
177
+ async findAnyWorkspaceId() {
178
+ let entries;
179
+ try {
180
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ for (const entry of entries) {
186
+ if (!entry.isDirectory())
187
+ continue;
188
+ try {
189
+ await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
190
+ return entry.name;
191
+ }
192
+ catch {
193
+ // No `.git` — half-built or unrelated dir; keep looking.
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ /**
199
+ * Run the clone. When a `referenceRepo` is supplied and the referenced
200
+ * clone fails (e.g. the sibling's object store is corrupt or mid-write),
201
+ * fall back once to a plain network clone so a bad sibling can never
202
+ * block a new branch from bootstrapping.
203
+ */
204
+ async runClone(targetDir, branch, referenceRepo) {
205
+ if (referenceRepo) {
206
+ try {
207
+ await execFileAsync('git', this.gitCloneArgs(targetDir, branch, referenceRepo), {
208
+ env: { ...process.env },
209
+ });
210
+ return;
211
+ }
212
+ catch (err) {
213
+ console.warn(`[workspace] referenced clone for "${branch}" failed, retrying without reference:`, redactError(err));
214
+ // Clear any partial output so the retry clones into a clean dir.
215
+ await fs.rm(targetDir, { recursive: true, force: true }).catch(() => { });
216
+ }
217
+ }
218
+ await execFileAsync('git', this.gitCloneArgs(targetDir, branch), {
219
+ env: { ...process.env },
220
+ });
221
+ }
222
+ /**
223
+ * Get-or-create the per-branch workspace. Idempotent — repeat calls for
224
+ * the same branch reuse the existing clone. Concurrent first-callers
225
+ * share one bootstrap via `inFlightBootstraps`.
226
+ */
227
+ async getOrCreateForBranch(branch) {
228
+ assertValidBranchName(branch);
229
+ const id = workspaceIdForBranch(branch);
230
+ const workspaceDir = path.join(this.workspacesRoot, id);
231
+ const repoDir = path.join(workspaceDir, this.kbDirName);
232
+ // Fast path: we've already bootstrapped this branch in this process.
233
+ // Wait for any in-flight bootstrap before returning so we never hand
234
+ // back a half-cloned workspace.
235
+ if (this.branchDirs.has(branch)) {
236
+ await this.awaitBootstrapOrEvict(branch);
237
+ return this.buildWorkspaceInfo(branch, workspaceDir);
238
+ }
239
+ // If a bootstrap is in flight for this branch, wait it out before
240
+ // trusting the on-disk check below. `git clone` creates `.git` within
241
+ // the first few hundred ms of a multi-second clone, well before the
242
+ // working tree (roles.yaml, access.md, every tracked file) is checked
243
+ // out — so a naive fs.access(.git) on a mid-clone dir happily hands a
244
+ // half-cloned workspace to the caller, and the next loadModel /
245
+ // readFile faceplants with ENOENT and 500s. This mirrors the
246
+ // single-flight wait the cached fast path already uses.
247
+ await this.awaitBootstrapOrEvict(branch);
248
+ if (this.branchDirs.has(branch)) {
249
+ return this.buildWorkspaceInfo(branch, workspaceDir);
250
+ }
251
+ // Check disk — the clone may have been bootstrapped by a previous
252
+ // process and survived a restart.
253
+ try {
254
+ await fs.access(path.join(repoDir, '.git'));
255
+ // Migration for clones already on disk: re-stamp the tracking config the
256
+ // first time this process opens them, so a clone whose config drifted
257
+ // (duplicate fetch refspec / merge ref) is repaired before anything
258
+ // pulls it. Once per branch per process — the cached paths above return
259
+ // before reaching here.
260
+ await this.normalizeCloneTracking(repoDir, branch);
261
+ this.branchDirs.set(branch, workspaceDir);
262
+ return this.buildWorkspaceInfo(branch, workspaceDir);
263
+ }
264
+ catch {
265
+ // Not on disk — bootstrap below.
266
+ }
267
+ // Single-flight bootstrap. Concurrent callers for the same branch
268
+ // share the first one's promise instead of stacking clones.
269
+ let resolveBootstrap;
270
+ let rejectBootstrap;
271
+ const bootstrap = new Promise((res, rej) => {
272
+ resolveBootstrap = res;
273
+ rejectBootstrap = rej;
274
+ });
275
+ bootstrap.catch(() => undefined);
276
+ const existingBootstrap = this.inFlightBootstraps.get(branch);
277
+ if (existingBootstrap) {
278
+ await existingBootstrap;
279
+ this.branchDirs.set(branch, workspaceDir);
280
+ return this.buildWorkspaceInfo(branch, workspaceDir);
281
+ }
282
+ this.inFlightBootstraps.set(branch, bootstrap);
283
+ try {
284
+ // Ensure the remote is seeded before the first clone of any branch. A
285
+ // clone with `-b <branch>` fails outright on an empty remote, so seeding
286
+ // must happen ahead of it. The seed service is single-flight, so this is
287
+ // a cheap already-resolved await on every clone after the first.
288
+ if (this.seedService) {
289
+ await this.seedService.ensureRemoteSeeded();
290
+ }
291
+ await fs.mkdir(workspaceDir, { recursive: true });
292
+ await this.cloneProcessMapForBranch(workspaceDir, branch);
293
+ this.branchDirs.set(branch, workspaceDir);
294
+ resolveBootstrap();
295
+ }
296
+ catch (err) {
297
+ this.branchDirs.delete(branch);
298
+ rejectBootstrap(err);
299
+ throw err;
300
+ }
301
+ finally {
302
+ if (this.inFlightBootstraps.get(branch) === bootstrap) {
303
+ this.inFlightBootstraps.delete(branch);
304
+ }
305
+ }
306
+ return this.buildWorkspaceInfo(branch, workspaceDir);
307
+ }
308
+ /**
309
+ * Convenience: open-or-create the workspace using the caller's user
310
+ * record only for typing / auditing context. Used by route handlers
311
+ * that don't know the target branch up-front (e.g. legacy bootstrap
312
+ * paths). Always resolves to the default protected branch unless
313
+ * `branch` is passed.
314
+ */
315
+ async getOrCreateForUser(_user, branch) {
316
+ return this.getOrCreateForBranch(branch ?? DEFAULT_BRANCH);
317
+ }
318
+ buildWorkspaceInfo(branch, workspaceDir) {
319
+ return {
320
+ id: workspaceIdForBranch(branch),
321
+ name: `${branch}`,
322
+ absolutePath: workspaceDir,
323
+ createdAt: new Date(0).toISOString(),
324
+ kbDirName: this.kbDirName,
325
+ };
326
+ }
327
+ /**
328
+ * Await the in-flight bootstrap for a branch (if any). On reject —
329
+ * meaning the clone rolled back — drop the branch→dir mapping so the
330
+ * next caller re-bootstraps.
331
+ */
332
+ async awaitBootstrapOrEvict(branch) {
333
+ const pending = this.inFlightBootstraps.get(branch);
334
+ if (!pending)
335
+ return;
336
+ try {
337
+ await pending;
338
+ }
339
+ catch (err) {
340
+ this.branchDirs.delete(branch);
341
+ throw err;
342
+ }
343
+ }
344
+ /**
345
+ * Collapse the clone's tracking config to a single fetch refspec and a single
346
+ * upstream ref for `branch` (see `workflow/git/clone-config.ts`). A clone that
347
+ * accumulated a second `remote.origin.fetch` refspec or `branch.<b>.merge`
348
+ * value makes git refuse to refresh it — "Cannot rebase onto multiple
349
+ * branches" — which is how a post-merge pull of the target branch fails.
350
+ *
351
+ * Never throws: it only ever touches `.git/config`, so a failure leaves the
352
+ * workspace exactly as usable as it was, and the git layer self-heals the
353
+ * same keys before it pulls.
354
+ */
355
+ async normalizeCloneTracking(repoDir, branch) {
356
+ try {
357
+ for (const args of cloneTrackingConfigArgs(branch)) {
358
+ await execFileAsync('git', ['-C', repoDir, ...args]);
359
+ }
360
+ }
361
+ catch (err) {
362
+ // One line per branch, not per key: every key writes to the same
363
+ // `.git/config`, so what fails for one fails for all.
364
+ console.warn(`[workspace] could not normalize the tracking config of the "${branch}" clone:`, redactError(err));
365
+ }
366
+ }
367
+ async cloneProcessMapForBranch(workspaceDir, branch) {
368
+ const targetDir = path.join(workspaceDir, this.kbDirName);
369
+ // Probe whether `targetDir` exists. We split the existence check from
370
+ // the inner `.git` validation so unexpected errors from EITHER probe
371
+ // propagate to the caller instead of getting silently swallowed by an
372
+ // outer catch (the previous shape let an inner rethrow fall through
373
+ // to a clone-into-non-empty-dir, producing a confusing 500).
374
+ let targetExists = true;
375
+ try {
376
+ await fs.access(targetDir);
377
+ }
378
+ catch (err) {
379
+ const code = err?.code;
380
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
381
+ targetExists = false;
382
+ }
383
+ else {
384
+ throw err;
385
+ }
386
+ }
387
+ if (targetExists) {
388
+ // The directory exists, but a previous bootstrap may have crashed
389
+ // mid-clone — leaving a directory shell without `.git`. Treat
390
+ // anything missing `.git` as not-cloned and re-clone. (Can't be
391
+ // an active workspace either: every cloned repo has `.git` from
392
+ // step one.)
393
+ let alreadyCloned = false;
394
+ try {
395
+ const dotGit = await fs.stat(path.join(targetDir, '.git'));
396
+ if (dotGit.isDirectory() || dotGit.isFile()) {
397
+ alreadyCloned = true;
398
+ }
399
+ }
400
+ catch (err) {
401
+ // Only treat "does-not-exist" as "half-built dir, wipe and
402
+ // re-clone". A transient EACCES / EIO / EBUSY must NOT delete
403
+ // what might be a perfectly valid repo whose `.git` we couldn't
404
+ // read this moment — surface the error so the caller can retry.
405
+ const code = err?.code;
406
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
407
+ throw err;
408
+ }
409
+ }
410
+ if (alreadyCloned) {
411
+ // We don't auto-pull because that could clobber another user's
412
+ // in-progress lock-held edits. The tracking config IS re-stamped —
413
+ // it never touches the working tree, and a drifted config is what
414
+ // breaks the next pull.
415
+ await this.normalizeCloneTracking(targetDir, branch);
416
+ return;
417
+ }
418
+ // Half-built dir — wipe and re-clone.
419
+ await fs.rm(targetDir, { recursive: true, force: true });
420
+ }
421
+ // Borrow an existing sibling clone's objects to skip the bulk download.
422
+ // Resolved before the clone so a failure mid-clone still rolls back cleanly.
423
+ const reference = await this.findSiblingRepo(branch);
424
+ try {
425
+ await this.runClone(targetDir, branch, reference);
426
+ // Persist longpaths in the cloned repo's config so subsequent
427
+ // checkouts also honor it.
428
+ await execFileAsync('git', ['-C', targetDir, 'config', 'core.longpaths', 'true']);
429
+ // Generic bot identity for the clone — every workflow commit
430
+ // overrides via `--author=…` so the real human shows up in
431
+ // `git log`. This is purely the fallback committer.
432
+ await execFileAsync('git', ['-C', targetDir, 'config', 'user.name', BOT_NAME]);
433
+ await execFileAsync('git', ['-C', targetDir, 'config', 'user.email', BOT_EMAIL]);
434
+ // Pin the clone to exactly one fetch refspec and one upstream ref for
435
+ // this branch. `git clone -b` already produces that shape; stamping it
436
+ // explicitly means the shape is asserted rather than assumed, and the
437
+ // same call is what repairs an existing clone that drifted.
438
+ await this.normalizeCloneTracking(targetDir, branch);
439
+ console.log(`[workspace] Cloned ${this.kbDirName} for branch "${branch}"` +
440
+ (reference ? ' (referenced a sibling clone)' : ''));
441
+ // A fresh clone has already downloaded every ref — tell the git layer
442
+ // so the first `listBranches` skips the redundant implicit `git fetch`.
443
+ // Isolated from the clone-rollback `catch` below: a misbehaving listener
444
+ // must not cause a successful clone to be wiped.
445
+ try {
446
+ this.onWorkspaceCloned?.(workspaceIdForBranch(branch));
447
+ }
448
+ catch (listenerErr) {
449
+ console.error(`[workspace] onWorkspaceCloned listener failed for branch "${branch}":`, redactError(listenerErr));
450
+ }
451
+ // Fill in any base scaffolding this branch is missing, now that it's
452
+ // freshly checked out. Runs on whichever branch the user loaded (not just
453
+ // protected ones), only on a fresh clone (this block). Isolated in its own
454
+ // try/catch — like the onWorkspaceCloned listener above — so even a future
455
+ // change that lets it throw can never roll back an otherwise-good clone.
456
+ if (this.seedService) {
457
+ try {
458
+ await this.seedService.topUpWorkspace(targetDir, branch);
459
+ }
460
+ catch (topUpErr) {
461
+ console.error(`[workspace] scaffolding top-up failed for branch "${branch}" (clone kept):`, redactError(topUpErr));
462
+ }
463
+ }
464
+ }
465
+ catch (err) {
466
+ const redacted = redactError(err);
467
+ console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
468
+ // Roll back partial state so the next bootstrap retries cleanly.
469
+ await fs.rm(targetDir, { recursive: true, force: true }).catch(() => { });
470
+ throw new Error(`Failed to clone process map: ${redacted}`);
471
+ }
472
+ }
473
+ async getMetadata(workspaceId) {
474
+ try {
475
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
476
+ return this.buildWorkspaceInfo(branchForWorkspaceId(workspaceId), workspaceDir);
477
+ }
478
+ catch {
479
+ return null;
480
+ }
481
+ }
482
+ /**
483
+ * Delete a branch's workspace directory entirely. Used when a branch is
484
+ * deleted upstream — frees the disk used by its clone. The branch's git
485
+ * history on origin is unaffected.
486
+ */
487
+ async deleteWorkspace(workspaceId) {
488
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
489
+ await fs.rm(workspaceDir, { recursive: true, force: true });
490
+ const branch = branchForWorkspaceId(workspaceId);
491
+ this.branchDirs.delete(branch);
492
+ }
493
+ async listFiles(workspaceId, readFilter) {
494
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
495
+ return this.buildFileTree(workspaceDir, workspaceDir, BevelIgnoreStack.empty(), readFilter);
496
+ }
497
+ async readFile(workspaceId, relativePath) {
498
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
499
+ const absolutePath = path.resolve(workspaceDir, relativePath);
500
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
501
+ return fs.readFile(absolutePath, 'utf-8');
502
+ }
503
+ async readFileBinary(workspaceId, relativePath) {
504
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
505
+ const absolutePath = path.resolve(workspaceDir, relativePath);
506
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
507
+ return fs.readFile(absolutePath);
508
+ }
509
+ /**
510
+ * Read every `.md` file under the KB repo in one disk pass, keyed by
511
+ * repo-root-relative POSIX path (e.g. `Product/NodeTypes/ServiceCommitment.md`)
512
+ * — the form `parseGraph` keys its file map on. Respects `.git/` and
513
+ * `.bevelignore` like the file tree, so the iframe's `buildGraph()` sees the
514
+ * same set the explorer shows. This exists so the HTML renderer can fetch the
515
+ * whole KB in a single request instead of one HTTP round-trip per file (the
516
+ * KB is thousands of files; per-file fetching exhausts the browser's
517
+ * connection pool).
518
+ */
519
+ async readAllKbFiles(workspaceId) {
520
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
521
+ const repoRoot = path.join(workspaceDir, this.kbDirName);
522
+ const files = {};
523
+ const walk = async (dir, ignoreStack) => {
524
+ const nextStack = await ignoreStack.extendedWith(dir);
525
+ const entries = await fs.readdir(dir, { withFileTypes: true });
526
+ for (const entry of entries) {
527
+ if (entry.name === '.git' && entry.isDirectory())
528
+ continue;
529
+ const childAbs = path.join(dir, entry.name);
530
+ if (nextStack.isIgnored(childAbs, entry.isDirectory()))
531
+ continue;
532
+ if (entry.isDirectory()) {
533
+ await walk(childAbs, nextStack);
534
+ continue;
535
+ }
536
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
537
+ continue;
538
+ const repoPath = path.relative(repoRoot, childAbs).replace(/\\/g, '/');
539
+ files[repoPath] = await fs.readFile(childAbs, 'utf-8');
540
+ }
541
+ };
542
+ await walk(repoRoot, BevelIgnoreStack.empty());
543
+ return files;
544
+ }
545
+ /**
546
+ * Build a zip archive of the folder at `relativePath` and return it as a
547
+ * single buffer. Mirrors the file-tree's skip rules — `.git/`, `.gitkeep`,
548
+ * and `.bevelignore`-excluded paths are omitted so the zip matches what
549
+ * the user sees in the explorer (and so we don't leak the entire git
550
+ * object database into a download).
551
+ *
552
+ * Entry names inside the zip are prefixed with the folder's own name so
553
+ * unzipping yields `<folderName>/...` rather than spilling files into the
554
+ * caller's cwd. Throws `FolderTooLargeError` if cumulative uncompressed
555
+ * size crosses `ZIP_DOWNLOAD_MAX_BYTES`. Buffered in memory (adm-zip has
556
+ * no streaming API); the cap therefore doubles as a peak-heap bound.
557
+ */
558
+ async createFolderZip(workspaceId, relativePath) {
559
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
560
+ const absoluteRoot = path.resolve(workspaceDir, relativePath);
561
+ this.assertWithinWorkspace(absoluteRoot, workspaceDir);
562
+ const stat = await fs.stat(absoluteRoot);
563
+ if (!stat.isDirectory()) {
564
+ throw new Error('Not a directory');
565
+ }
566
+ const zipRoot = path.basename(absoluteRoot) || 'folder';
567
+ const zip = new AdmZip();
568
+ let totalBytes = 0;
569
+ const walk = async (dir, ignoreStack) => {
570
+ const nextStack = await ignoreStack.extendedWith(dir);
571
+ const entries = await fs.readdir(dir, { withFileTypes: true });
572
+ entries.sort((a, b) => a.name.localeCompare(b.name));
573
+ for (const entry of entries) {
574
+ if (entry.name === '.git' && entry.isDirectory())
575
+ continue;
576
+ if (entry.name === '.gitkeep' && entry.isFile())
577
+ continue;
578
+ const childAbs = path.join(dir, entry.name);
579
+ if (nextStack.isIgnored(childAbs, entry.isDirectory()))
580
+ continue;
581
+ if (entry.isDirectory()) {
582
+ await walk(childAbs, nextStack);
583
+ continue;
584
+ }
585
+ if (!entry.isFile())
586
+ continue; // skip sockets, symlinks, etc.
587
+ // Check the size cap BEFORE reading the file into memory. Reading
588
+ // first would let a single hostile 2 GB file allocate the whole
589
+ // buffer before the throw — defeating the cap as a peak-heap
590
+ // bound. `stat.size` is an upper bound that we re-verify after
591
+ // the read in case the file grew between stat and readFile.
592
+ const stat = await fs.stat(childAbs);
593
+ if (totalBytes + stat.size > ZIP_DOWNLOAD_MAX_BYTES) {
594
+ throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
595
+ }
596
+ const data = await fs.readFile(childAbs);
597
+ if (totalBytes + data.byteLength > ZIP_DOWNLOAD_MAX_BYTES) {
598
+ throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
599
+ }
600
+ totalBytes += data.byteLength;
601
+ // Path inside the archive: <folderName>/<relPathUnderFolder>, POSIX
602
+ // separators regardless of host OS so the zip extracts cleanly
603
+ // on Windows/Mac/Linux alike.
604
+ const relInside = path
605
+ .relative(absoluteRoot, childAbs)
606
+ .replace(/\\/g, '/');
607
+ zip.addFile(`${zipRoot}/${relInside}`, data);
608
+ }
609
+ };
610
+ await walk(absoluteRoot, BevelIgnoreStack.empty());
611
+ return zip.toBuffer();
612
+ }
613
+ /**
614
+ * Read a file's contents as it exists on a specific git ref in the
615
+ * branch's clone. Returns null when the ref or path doesn't exist on that
616
+ * ref. `relativePath` is relative to the repo root (e.g. `Knowledge/Foo.md`),
617
+ * NOT to the workspace dir — the repo sits at `<workspaceDir>/knowledge-base`.
618
+ */
619
+ async readFileAtRef(workspaceId, ref, relativePath) {
620
+ this.assertValidGitRef(ref);
621
+ this.assertValidRepoRelativePath(relativePath);
622
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
623
+ const repoDir = path.join(workspaceDir, this.kbDirName);
624
+ const candidates = [ref];
625
+ if (!ref.startsWith('origin/') && !ref.startsWith('refs/')) {
626
+ candidates.push(`origin/${ref}`);
627
+ }
628
+ for (const candidate of candidates) {
629
+ try {
630
+ const { stdout } = await execFileAsync('git', ['-C', repoDir, 'show', `${candidate}:${relativePath}`], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
631
+ return stdout;
632
+ }
633
+ catch {
634
+ // Try next candidate.
635
+ }
636
+ }
637
+ return null;
638
+ }
639
+ /**
640
+ * Run `git fetch --prune origin` for this branch's clone. Results are
641
+ * cached for `FETCH_CACHE_TTL_MS`; concurrent callers share the same
642
+ * in-flight fetch to avoid fetch storms when e.g. the CR list poll fans out.
643
+ */
644
+ async ensureRemotesFetched(workspaceId) {
645
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
646
+ const repoDir = path.join(workspaceDir, this.kbDirName);
647
+ const now = Date.now();
648
+ const last = this.lastFetchAt.get(repoDir) ?? 0;
649
+ if (now - last < FETCH_CACHE_TTL_MS)
650
+ return;
651
+ const inFlight = this.inFlightFetches.get(repoDir);
652
+ if (inFlight)
653
+ return inFlight;
654
+ // This driver runs outside the git layer's per-workspace mutex, so it may
655
+ // only run the safe implicit-fetch shape — see `SAFE_IMPLICIT_FETCH_ARGS`
656
+ // in `workflow/git/clone-config.ts` for the full rationale.
657
+ const promise = execFileAsync('git', ['-C', repoDir, ...SAFE_IMPLICIT_FETCH_ARGS], { env: { ...process.env } })
658
+ .then(() => {
659
+ this.lastFetchAt.set(repoDir, Date.now());
660
+ })
661
+ .catch((err) => {
662
+ console.warn('[workspace] git fetch origin failed:', redactError(err));
663
+ })
664
+ .finally(() => {
665
+ if (this.inFlightFetches.get(repoDir) === promise) {
666
+ this.inFlightFetches.delete(repoDir);
667
+ }
668
+ });
669
+ this.inFlightFetches.set(repoDir, promise);
670
+ return promise;
671
+ }
672
+ assertValidGitRef(ref) {
673
+ if (!ref)
674
+ throw new Error('Git ref is required');
675
+ if (ref.startsWith('-') || /[\s\0]/.test(ref)) {
676
+ throw new Error('Invalid git ref');
677
+ }
678
+ }
679
+ assertValidRepoRelativePath(relativePath) {
680
+ if (!relativePath)
681
+ throw new Error('Repo-relative path is required');
682
+ if (relativePath.startsWith('-') || relativePath.includes('\0')) {
683
+ throw new Error('Invalid repo-relative path');
684
+ }
685
+ }
686
+ async deleteFile(workspaceId, relativePath) {
687
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
688
+ const absolutePath = path.resolve(workspaceDir, relativePath);
689
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
690
+ await fs.rm(absolutePath, { recursive: true, force: true });
691
+ await this.diffService?.markUserDeleted(workspaceId, relativePath);
692
+ }
693
+ /**
694
+ * Remove the workspace's `tmp/` directory — transient run artifacts (code-mode
695
+ * tool-chain spills, staged SharePoint docs) that live at the workspace root,
696
+ * outside the KB git repo, and are never committed. Computes the path directly
697
+ * from `workspacesRoot` (no bootstrapping) and force-removes, so it's safe to
698
+ * call after a run even if the workspace was already torn down — a missing
699
+ * directory is ignored.
700
+ */
701
+ async clearTmp(workspaceId) {
702
+ // Guard against a malicious/odd workspaceId (e.g. "..") escaping the root
703
+ // and force-removing an unintended directory.
704
+ const root = path.resolve(this.workspacesRoot);
705
+ const tmpDir = path.resolve(root, workspaceId, 'tmp');
706
+ if (!tmpDir.startsWith(root + path.sep)) {
707
+ throw new Error(`clearTmp: refusing to remove a path outside the workspaces root (workspaceId="${workspaceId}").`);
708
+ }
709
+ await fs.rm(tmpDir, { recursive: true, force: true });
710
+ }
711
+ async moveEntry(workspaceId, oldRelativePath, newRelativePath) {
712
+ assertValidRelativePath(newRelativePath);
713
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
714
+ const oldAbsolute = path.resolve(workspaceDir, oldRelativePath);
715
+ const newAbsolute = path.resolve(workspaceDir, newRelativePath);
716
+ this.assertWithinWorkspace(oldAbsolute, workspaceDir);
717
+ this.assertWithinWorkspace(newAbsolute, workspaceDir);
718
+ await fs.mkdir(path.dirname(newAbsolute), { recursive: true });
719
+ await fs.rename(oldAbsolute, newAbsolute);
720
+ if (this.diffService) {
721
+ await this.diffService.markUserDeleted(workspaceId, oldRelativePath);
722
+ await this.diffService.syncFromDisk(workspaceId, newRelativePath);
723
+ }
724
+ }
725
+ async writeFile(workspaceId, relativePath, content) {
726
+ assertValidRelativePath(relativePath);
727
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
728
+ const absolutePath = path.resolve(workspaceDir, relativePath);
729
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
730
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
731
+ await fs.writeFile(absolutePath, content, 'utf-8');
732
+ await this.diffService?.syncFromDisk(workspaceId, relativePath);
733
+ }
734
+ async createDirectory(workspaceId, relativePath) {
735
+ assertValidRelativePath(relativePath);
736
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
737
+ const absolutePath = path.resolve(workspaceDir, relativePath);
738
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
739
+ await fs.mkdir(absolutePath, { recursive: true });
740
+ const entries = await fs.readdir(absolutePath);
741
+ if (entries.length === 0) {
742
+ await fs.writeFile(path.join(absolutePath, '.gitkeep'), '', 'utf-8');
743
+ }
744
+ }
745
+ async writeFileBinary(workspaceId, relativePath, data) {
746
+ assertValidRelativePath(relativePath);
747
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
748
+ const absolutePath = path.resolve(workspaceDir, relativePath);
749
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
750
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
751
+ await fs.writeFile(absolutePath, data);
752
+ await this.diffService?.syncFromDisk(workspaceId, relativePath);
753
+ }
754
+ /**
755
+ * Extract a `.zip` file already present in the workspace into a destination
756
+ * directory (defaults to the archive's parent directory — "unzip here").
757
+ * Validation + zip-bomb guards are identical to the per-user version that
758
+ * preceded the workspace-per-branch migration.
759
+ */
760
+ async unzipFile(workspaceId, zipRelativePath, destDirRelativePath,
761
+ /**
762
+ * Per-extracted-path write guard (ontology-session boundary). Called with
763
+ * each entry's workspace-relative target before it is written; if it throws,
764
+ * the entry is skipped (with the thrown message as the reason) rather than
765
+ * aborting the whole extraction. Omitted by non-agent / human callers.
766
+ */
767
+ guardWrite) {
768
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
769
+ const zipAbsolute = path.resolve(workspaceDir, zipRelativePath);
770
+ this.assertWithinWorkspace(zipAbsolute, workspaceDir);
771
+ if (!zipRelativePath.toLowerCase().endsWith('.zip')) {
772
+ throw new Error('Only .zip files can be extracted');
773
+ }
774
+ const inferredDest = (() => {
775
+ const d = path.posix.dirname(zipRelativePath.replace(/\\/g, '/'));
776
+ return d === '.' ? '' : d;
777
+ })();
778
+ const destRel = destDirRelativePath ?? inferredDest;
779
+ if (destRel)
780
+ assertValidRelativePath(destRel);
781
+ const destAbsolute = destRel ? path.resolve(workspaceDir, destRel) : workspaceDir;
782
+ this.assertWithinWorkspace(destAbsolute, workspaceDir);
783
+ let zip;
784
+ try {
785
+ zip = new AdmZip(zipAbsolute);
786
+ }
787
+ catch (err) {
788
+ const msg = err instanceof Error ? err.message : String(err);
789
+ throw new Error(`Could not read zip file: ${msg}`);
790
+ }
791
+ // The destination directory is created on demand by the first allowed entry
792
+ // (each guarded path mkdirs its parents with `recursive: true`). Creating it
793
+ // up front would leave it behind even when every entry is write-blocked.
794
+ const extracted = [];
795
+ const skipped = [];
796
+ let processedCount = 0;
797
+ let totalUncompressed = 0;
798
+ for (const entry of zip.getEntries()) {
799
+ if (processedCount >= UNZIP_MAX_ENTRIES) {
800
+ skipped.push({
801
+ path: entry.entryName,
802
+ reason: `Archive exceeds ${UNZIP_MAX_ENTRIES} entries`,
803
+ });
804
+ continue;
805
+ }
806
+ processedCount++;
807
+ const rawName = entry.entryName.replace(/\\/g, '/');
808
+ if (rawName.startsWith('__MACOSX/') ||
809
+ rawName === '__MACOSX' ||
810
+ rawName.endsWith('/.DS_Store') ||
811
+ rawName === '.DS_Store' ||
812
+ /(^|\/)\._/.test(rawName)) {
813
+ continue;
814
+ }
815
+ if (!rawName || rawName.startsWith('/') || /(^|\/)\.\.($|\/)/.test(rawName)) {
816
+ skipped.push({ path: rawName || '(empty)', reason: 'Invalid path' });
817
+ continue;
818
+ }
819
+ const trimmed = rawName.replace(/\/+$/, '');
820
+ const segments = trimmed.split('/').filter((s) => s.length > 0);
821
+ let invalidReason = null;
822
+ for (const segment of segments) {
823
+ const reason = validateFilename(segment);
824
+ if (reason) {
825
+ invalidReason = reason;
826
+ break;
827
+ }
828
+ }
829
+ if (invalidReason) {
830
+ skipped.push({ path: rawName, reason: invalidReason });
831
+ continue;
832
+ }
833
+ const targetAbsolute = path.resolve(destAbsolute, ...segments);
834
+ const destRoot = path.resolve(destAbsolute);
835
+ if (targetAbsolute !== destRoot && !targetAbsolute.startsWith(destRoot + path.sep)) {
836
+ skipped.push({ path: rawName, reason: 'Path escapes destination' });
837
+ continue;
838
+ }
839
+ const relForReport = path
840
+ .relative(workspaceDir, targetAbsolute)
841
+ .replace(/\\/g, '/');
842
+ // Ontology-session boundary: a write-blocked (or cross-ontology) entry is
843
+ // skipped, not extracted, so an archive can't be a write path around it.
844
+ // Runs before any filesystem side effect — including directory creation —
845
+ // so a blocked entry leaves nothing behind on disk.
846
+ const allowEntry = async () => {
847
+ if (!guardWrite)
848
+ return true;
849
+ try {
850
+ await guardWrite(relForReport);
851
+ return true;
852
+ }
853
+ catch (err) {
854
+ skipped.push({ path: rawName, reason: err instanceof Error ? err.message : 'Blocked by the ontology-session boundary' });
855
+ return false;
856
+ }
857
+ };
858
+ if (entry.isDirectory) {
859
+ if (!(await allowEntry()))
860
+ continue;
861
+ await fs.mkdir(targetAbsolute, { recursive: true });
862
+ continue;
863
+ }
864
+ const declaredSize = entry.header?.size ?? 0;
865
+ if (declaredSize > UNZIP_MAX_ENTRY_BYTES) {
866
+ skipped.push({
867
+ path: rawName,
868
+ reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
869
+ });
870
+ continue;
871
+ }
872
+ if (totalUncompressed + declaredSize > UNZIP_MAX_TOTAL_BYTES) {
873
+ skipped.push({
874
+ path: rawName,
875
+ reason: `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`,
876
+ });
877
+ continue;
878
+ }
879
+ const data = entry.getData();
880
+ if (data.byteLength > UNZIP_MAX_ENTRY_BYTES) {
881
+ skipped.push({
882
+ path: rawName,
883
+ reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
884
+ });
885
+ continue;
886
+ }
887
+ if (totalUncompressed + data.byteLength > UNZIP_MAX_TOTAL_BYTES) {
888
+ skipped.push({
889
+ path: rawName,
890
+ reason: `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`,
891
+ });
892
+ continue;
893
+ }
894
+ if (!(await allowEntry()))
895
+ continue;
896
+ await fs.mkdir(path.dirname(targetAbsolute), { recursive: true });
897
+ await fs.writeFile(targetAbsolute, data);
898
+ totalUncompressed += data.byteLength;
899
+ extracted.push(relForReport);
900
+ await this.diffService?.syncFromDisk(workspaceId, relForReport);
901
+ }
902
+ return {
903
+ destination: path.relative(workspaceDir, destAbsolute).replace(/\\/g, '/') || '.',
904
+ extracted,
905
+ skipped,
906
+ };
907
+ }
908
+ async getWorkspacePath(workspaceId) {
909
+ return this.resolveWorkspaceDir(workspaceId);
910
+ }
911
+ /**
912
+ * True when `workspaceId` already has a bootstrapped clone — either in the
913
+ * in-process cache or persisted on disk — WITHOUT triggering the lazy
914
+ * bootstrap that `resolveWorkspaceDir` does. Lets best-effort callers
915
+ * (e.g. chat-upload staging) refuse to clone an arbitrary branch supplied
916
+ * in a request body. Per-branch access itself is open by design, so this is
917
+ * an IO guard, not an ownership check.
918
+ */
919
+ async hasBootstrappedWorkspace(workspaceId) {
920
+ if (!workspaceId)
921
+ return false;
922
+ let branch;
923
+ try {
924
+ branch = branchForWorkspaceId(workspaceId);
925
+ }
926
+ catch {
927
+ return false;
928
+ }
929
+ if (this.branchDirs.has(branch))
930
+ return true;
931
+ let workspaceDir;
932
+ try {
933
+ workspaceDir = this.workspaceDirWithinRoot(workspaceId);
934
+ }
935
+ catch {
936
+ return false;
937
+ }
938
+ try {
939
+ await fs.access(path.join(workspaceDir, this.kbDirName, '.git'));
940
+ return true;
941
+ }
942
+ catch {
943
+ return false;
944
+ }
945
+ }
946
+ /**
947
+ * Enumerate every branch this process has bootstrapped a workspace for.
948
+ * Used by the `PendingCommitsWorker` to know which workspaces to poll
949
+ * and by `startupReconcile` to know which workspaces to scan for
950
+ * orphan files. The returned descriptors match the worker's
951
+ * `WorkspaceProvider` contract — id + branch in one struct, no need
952
+ * for the caller to re-encode.
953
+ *
954
+ * The cache is populated lazily as `getOrCreateForBranch` resolves, so
955
+ * a freshly-touched branch shows up here within the same poll cycle
956
+ * that resolved its bootstrap.
957
+ */
958
+ knownWorkspaces() {
959
+ return Array.from(this.branchDirs.keys()).map((branch) => ({
960
+ id: workspaceIdForBranch(branch),
961
+ branch,
962
+ }));
963
+ }
964
+ /**
965
+ * Walk the working tree of `workspaceId`'s clone and return every
966
+ * path `git status --porcelain=v1` reports as dirty (untracked,
967
+ * modified, or staged). Used by `PendingCommitsService.startupReconcile`
968
+ * to enqueue commits for files left orphaned by a prior process
969
+ * crash — under save=share + the pending-commits queue the working
970
+ * tree should never be dirty in steady state, so anything reported
971
+ * here is recovery material.
972
+ *
973
+ * Paths are workspace-relative (`<kbDirName>/<repo-relative>`), matching
974
+ * the form `releaseLock` expects.
975
+ *
976
+ * Returns an empty array if the workspace dir isn't yet bootstrapped
977
+ * or the repo dir is missing; the worker treats that as "nothing to
978
+ * recover here" and moves on.
979
+ */
980
+ async scanOrphanedPaths(workspaceId) {
981
+ let repoDir;
982
+ try {
983
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
984
+ repoDir = path.join(workspaceDir, this.kbDirName);
985
+ // Confirm the inner repo dir actually exists — getOrCreateForBranch
986
+ // may have failed mid-bootstrap and we'd otherwise hit a confusing
987
+ // git error here.
988
+ await fs.access(path.join(repoDir, '.git'));
989
+ }
990
+ catch {
991
+ return [];
992
+ }
993
+ let stdout;
994
+ try {
995
+ ({ stdout } = await execFileAsync('git', ['-C', repoDir, 'status', '--porcelain=v1', '-z'], {
996
+ encoding: 'utf-8',
997
+ maxBuffer: 16 * 1024 * 1024,
998
+ }));
999
+ }
1000
+ catch (err) {
1001
+ console.warn(`[workspace] orphan scan via git status failed for ${workspaceId}:`, err instanceof Error ? err.message : err);
1002
+ return [];
1003
+ }
1004
+ if (!stdout.trim())
1005
+ return [];
1006
+ // `-z` separates entries with NUL and skips quoting; renames use a
1007
+ // second NUL-terminated field for the original path which we ignore.
1008
+ const entries = stdout.split('\0').filter((s) => s.length > 0);
1009
+ const out = [];
1010
+ for (let i = 0; i < entries.length; i++) {
1011
+ const entry = entries[i];
1012
+ // First 3 chars are status codes; the rest is the path.
1013
+ if (entry.length < 4)
1014
+ continue;
1015
+ const code = entry.slice(0, 2);
1016
+ const repoRelative = entry.slice(3);
1017
+ // Renames + copies carry the original path as the next NUL-separated
1018
+ // field. Skip it — we want the new path only.
1019
+ if (code[0] === 'R' || code[0] === 'C') {
1020
+ i += 1;
1021
+ }
1022
+ out.push({ path: `${this.kbDirName}/${repoRelative}` });
1023
+ }
1024
+ return out;
1025
+ }
1026
+ /**
1027
+ * Remove on-disk workspace dirs whose branches aren't in `knownBranches`.
1028
+ * Handles two drift sources:
1029
+ * - Branch deleted on origin while the backend was offline (no live
1030
+ * `deleteBranch` call ran, so the clone wasn't cleaned up).
1031
+ * - Local-only experimentation (a branch that never made it to origin).
1032
+ *
1033
+ * `knownBranches` is the set of branch names the caller considers
1034
+ * authoritative — typically the union of local + remote branches from
1035
+ * a recent `git for-each-ref` against one of the live clones.
1036
+ *
1037
+ * Best-effort: removal failures are logged and swallowed so one stuck
1038
+ * directory doesn't poison the whole sweep. Returns the set of removed
1039
+ * workspace ids so callers can log / report cleanup activity.
1040
+ */
1041
+ async sweepOrphanedWorkspaces(knownBranches) {
1042
+ const validIds = new Set();
1043
+ for (const b of knownBranches)
1044
+ validIds.add(workspaceIdForBranch(b));
1045
+ let entries;
1046
+ try {
1047
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
1048
+ }
1049
+ catch {
1050
+ return { removed: [] };
1051
+ }
1052
+ const removed = [];
1053
+ for (const entry of entries) {
1054
+ if (!entry.isDirectory())
1055
+ continue;
1056
+ if (validIds.has(entry.name))
1057
+ continue;
1058
+ // Skip dirs that don't look like an encoded workspace id — third-
1059
+ // party tooling sometimes drops scratch dirs alongside ours.
1060
+ try {
1061
+ decodeURIComponent(entry.name);
1062
+ }
1063
+ catch {
1064
+ continue;
1065
+ }
1066
+ const dir = path.join(this.workspacesRoot, entry.name);
1067
+ try {
1068
+ await fs.rm(dir, { recursive: true, force: true });
1069
+ this.branchDirs.delete(branchForWorkspaceId(entry.name));
1070
+ removed.push(entry.name);
1071
+ }
1072
+ catch (err) {
1073
+ console.warn(`[workspace] sweep failed for ${entry.name}:`, err instanceof Error ? err.message : err);
1074
+ }
1075
+ }
1076
+ return { removed };
1077
+ }
1078
+ /**
1079
+ * Resolve a workspace id to its on-disk directory. `workspaceId` is
1080
+ * `encodeURIComponent(branch)` — the decoded branch is the in-memory
1081
+ * cache key, the encoded form is the disk directory name.
1082
+ *
1083
+ * Resolution order:
1084
+ * 1. In-process cache (`branchDirs`) — bootstrap completed earlier this
1085
+ * run, or a prior `resolveWorkspaceDir` populated it from disk.
1086
+ * 2. On-disk lookup — the directory may persist across container
1087
+ * restarts; if `<id>/<kbDirName>/.git` is there, register and return.
1088
+ * 3. **Lazy bootstrap** — clone the branch on demand via
1089
+ * `getOrCreateForBranch`. Without this, any frontend route that
1090
+ * hits `/api/workspace/:id/workflow/...` for a branch the user
1091
+ * hasn't explicitly bootstrapped via `GET /api/workspace?branch=…`
1092
+ * would 500 with "Invalid workspace ID" (real symptom in dev:
1093
+ * `listBranches` / `branch-status` failing right after container
1094
+ * start because the frontend's parallel calls race the initial
1095
+ * bootstrap). Bootstrap is single-flight so concurrent racers
1096
+ * share one clone. A bogus branch name surfaces the underlying
1097
+ * `BranchNameError` / clone failure verbatim — much more useful
1098
+ * than the old generic "Invalid workspace ID".
1099
+ *
1100
+ * Cold-cache cost: a mid-request clone is multi-second. Subsequent
1101
+ * requests use the cache. Acceptable since it only happens once per
1102
+ * branch per container lifetime.
1103
+ */
1104
+ /**
1105
+ * Resolve `<workspacesRoot>/<workspaceId>` and assert it stays under the
1106
+ * root. `workspaceId` is expected to be `encodeURIComponent(branch)`, but a
1107
+ * malformed caller value (e.g. `..`) could otherwise escape the root before
1108
+ * the `.git` probe runs. Mirrors `clearTmp`'s containment guard.
1109
+ */
1110
+ workspaceDirWithinRoot(workspaceId) {
1111
+ const root = path.resolve(this.workspacesRoot);
1112
+ const dir = path.resolve(root, workspaceId);
1113
+ if (dir !== root && !dir.startsWith(root + path.sep)) {
1114
+ throw new Error(`Invalid workspace ID "${workspaceId}": path escapes the workspaces root.`);
1115
+ }
1116
+ return dir;
1117
+ }
1118
+ async resolveWorkspaceDir(workspaceId) {
1119
+ if (!workspaceId)
1120
+ throw new Error('Invalid workspace ID');
1121
+ const branch = branchForWorkspaceId(workspaceId);
1122
+ if (this.branchDirs.has(branch)) {
1123
+ return this.branchDirs.get(branch);
1124
+ }
1125
+ // Wait for any in-flight bootstrap before trusting the on-disk check.
1126
+ // `git clone` creates `.git` early in a multi-second clone, before the
1127
+ // working tree files (roles.yaml, etc.) are checked out — so a naive
1128
+ // fs.access(.git) on a mid-clone dir returns "ready" and the caller's
1129
+ // next readFile faceplants with ENOENT and a 500. Mirrors the
1130
+ // single-flight wait inside `getOrCreateForBranch`.
1131
+ await this.awaitBootstrapOrEvict(branch).catch(() => undefined);
1132
+ if (this.branchDirs.has(branch)) {
1133
+ return this.branchDirs.get(branch);
1134
+ }
1135
+ const workspaceDir = this.workspaceDirWithinRoot(workspaceId);
1136
+ try {
1137
+ await fs.access(path.join(workspaceDir, this.kbDirName, '.git'));
1138
+ this.branchDirs.set(branch, workspaceDir);
1139
+ return workspaceDir;
1140
+ }
1141
+ catch {
1142
+ // Fall through to lazy bootstrap.
1143
+ }
1144
+ try {
1145
+ await this.getOrCreateForBranch(branch);
1146
+ const dir = this.branchDirs.get(branch);
1147
+ if (dir)
1148
+ return dir;
1149
+ // Bootstrap reported success but didn't register — defensive fallback.
1150
+ return workspaceDir;
1151
+ }
1152
+ catch (err) {
1153
+ const reason = err instanceof Error ? err.message : String(err);
1154
+ throw new Error(`Invalid workspace ID "${workspaceId}": ${reason}`);
1155
+ }
1156
+ }
1157
+ assertWithinWorkspace(absolutePath, workspaceDir) {
1158
+ const resolved = path.resolve(absolutePath);
1159
+ const root = path.resolve(workspaceDir);
1160
+ if (!resolved.startsWith(root + path.sep) && resolved !== root) {
1161
+ throw new Error('Path traversal detected');
1162
+ }
1163
+ }
1164
+ async buildFileTree(dir, workspaceRoot, parentIgnore, readFilter) {
1165
+ const name = path.basename(dir);
1166
+ const relativePath = path.relative(workspaceRoot, dir).replace(/\\/g, '/');
1167
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1168
+ const ignoreStack = await parentIgnore.extendedWith(dir);
1169
+ // First pass: the entries surviving `.git`/`.gitkeep` + `.bevelignore`.
1170
+ const candidates = [];
1171
+ for (const entry of entries) {
1172
+ // `.git/` is never user content and listing it would blow up the
1173
+ // tree response. `.gitkeep` is the empty-folder placeholder — not
1174
+ // worth surfacing in the file list.
1175
+ if (entry.name === '.git' && entry.isDirectory())
1176
+ continue;
1177
+ if (entry.name === '.gitkeep' && entry.isFile())
1178
+ continue;
1179
+ const entryPath = path.join(dir, entry.name);
1180
+ if (ignoreStack.isIgnored(entryPath, entry.isDirectory()))
1181
+ continue;
1182
+ candidates.push({
1183
+ entry,
1184
+ entryPath,
1185
+ rel: path.relative(workspaceRoot, entryPath).replace(/\\/g, '/'),
1186
+ });
1187
+ }
1188
+ // Read-permission filter: ONE batched check per directory. Drops files AND
1189
+ // directories the caller can't read; a hidden directory's subtree is never
1190
+ // walked (skip-and-don't-recurse). A readable directory left empty after
1191
+ // filtering stays visible (the folder itself is readable). Fail-closed:
1192
+ // anything not explicitly readable is dropped. No filter → identical to the
1193
+ // pre-feature tree (regression-safe).
1194
+ let visible = candidates;
1195
+ if (readFilter && candidates.length > 0) {
1196
+ const verdict = await readFilter(candidates.map((c) => c.rel));
1197
+ visible = candidates.filter((c) => verdict.get(c.rel) === true);
1198
+ }
1199
+ const children = [];
1200
+ for (const { entry, entryPath, rel } of visible) {
1201
+ if (entry.isDirectory()) {
1202
+ children.push(await this.buildFileTree(entryPath, workspaceRoot, ignoreStack, readFilter));
1203
+ }
1204
+ else {
1205
+ children.push({ name: entry.name, relativePath: rel, type: 'file' });
1206
+ }
1207
+ }
1208
+ children.sort((a, b) => {
1209
+ if (a.type !== b.type)
1210
+ return a.type === 'directory' ? -1 : 1;
1211
+ return a.name.localeCompare(b.name);
1212
+ });
1213
+ return {
1214
+ name,
1215
+ relativePath: relativePath || '.',
1216
+ type: 'directory',
1217
+ children,
1218
+ };
1219
+ }
1220
+ }
1221
+ //# sourceMappingURL=workspace.service.js.map