@bevel-software/platform-core-backend 0.15.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1220) hide show
  1. package/THIRD-PARTY-NOTICES.md +4 -4
  2. package/dist/core/catalog-cache-invalidation.d.ts +16 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
  4. package/dist/core/catalog-cache-invalidation.js +6 -1
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -1
  6. package/dist/core/catalog-revision.d.ts +39 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -0
  8. package/dist/core/catalog-revision.js +94 -0
  9. package/dist/core/catalog-revision.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +7 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +199 -75
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +49 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +212 -86
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +138 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -0
  20. package/dist/core/lifecycle.js +218 -0
  21. package/dist/core/lifecycle.js.map +1 -0
  22. package/dist/core/public-config.d.ts +82 -0
  23. package/dist/core/public-config.d.ts.map +1 -0
  24. package/dist/core/public-config.js +78 -0
  25. package/dist/core/public-config.js.map +1 -0
  26. package/dist/core/readiness.d.ts +86 -0
  27. package/dist/core/readiness.d.ts.map +1 -0
  28. package/dist/core/readiness.js +56 -0
  29. package/dist/core/readiness.js.map +1 -0
  30. package/dist/core-config.d.ts +30 -0
  31. package/dist/core-config.d.ts.map +1 -1
  32. package/dist/core-config.js +64 -2
  33. package/dist/core-config.js.map +1 -1
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/access/access-control.interface.d.ts +215 -11
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +57 -11
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +616 -155
  43. package/dist/modules/access/access-control.service.js.map +1 -1
  44. package/dist/modules/access/access-mutation.service.d.ts +71 -5
  45. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-mutation.service.js +161 -13
  47. package/dist/modules/access/access-mutation.service.js.map +1 -1
  48. package/dist/modules/access/access-view.d.ts +147 -0
  49. package/dist/modules/access/access-view.d.ts.map +1 -0
  50. package/dist/modules/access/access-view.js +211 -0
  51. package/dist/modules/access/access-view.js.map +1 -0
  52. package/dist/modules/access/access.routes.d.ts.map +1 -1
  53. package/dist/modules/access/access.routes.js +255 -79
  54. package/dist/modules/access/access.routes.js.map +1 -1
  55. package/dist/modules/access/admin-locked-commit.d.ts +2 -3
  56. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  57. package/dist/modules/access/admin-locked-commit.js +9 -8
  58. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  59. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
  60. package/dist/modules/access/admin-route-helpers.js +4 -4
  61. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  62. package/dist/modules/access/change-read-gate.d.ts +53 -0
  63. package/dist/modules/access/change-read-gate.d.ts.map +1 -0
  64. package/dist/modules/access/change-read-gate.js +134 -0
  65. package/dist/modules/access/change-read-gate.js.map +1 -0
  66. package/dist/modules/access/creator-access.d.ts +33 -39
  67. package/dist/modules/access/creator-access.d.ts.map +1 -1
  68. package/dist/modules/access/creator-access.js +92 -119
  69. package/dist/modules/access/creator-access.js.map +1 -1
  70. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  71. package/dist/modules/access/directory-sync-bot.js +3 -1
  72. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  73. package/dist/modules/access/groups-admin.service.d.ts +3 -3
  74. package/dist/modules/access/groups-admin.service.js +2 -2
  75. package/dist/modules/access/reference-scan.d.ts +1 -1
  76. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  77. package/dist/modules/access/reference-scan.js +5 -4
  78. package/dist/modules/access/reference-scan.js.map +1 -1
  79. package/dist/modules/access/roles-admin.service.d.ts +72 -13
  80. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  81. package/dist/modules/access/roles-admin.service.js +71 -13
  82. package/dist/modules/access/roles-admin.service.js.map +1 -1
  83. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  84. package/dist/modules/access/synced-groups-committer.js +8 -6
  85. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
  87. package/dist/modules/access/synced-groups-writer.js +3 -8
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  89. package/dist/modules/access/user-access-removal.service.d.ts +209 -0
  90. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
  91. package/dist/modules/access/user-access-removal.service.js +662 -0
  92. package/dist/modules/access/user-access-removal.service.js.map +1 -0
  93. package/dist/modules/access-model/access-errors.d.ts +32 -1
  94. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  95. package/dist/modules/access-model/access-errors.js +38 -11
  96. package/dist/modules/access-model/access-errors.js.map +1 -1
  97. package/dist/modules/access-model/access-grammar.d.ts +49 -24
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +68 -48
  100. package/dist/modules/access-model/access-grammar.js.map +1 -1
  101. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  102. package/dist/modules/access-model/access-splice.js +18 -18
  103. package/dist/modules/access-model/access-splice.js.map +1 -1
  104. package/dist/modules/access-model/access-template.d.ts +22 -0
  105. package/dist/modules/access-model/access-template.d.ts.map +1 -0
  106. package/dist/modules/access-model/access-template.js +42 -0
  107. package/dist/modules/access-model/access-template.js.map +1 -0
  108. package/dist/modules/access-model/change-gate.d.ts +65 -0
  109. package/dist/modules/access-model/change-gate.d.ts.map +1 -0
  110. package/dist/modules/access-model/change-gate.js +25 -0
  111. package/dist/modules/access-model/change-gate.js.map +1 -0
  112. package/dist/modules/access-model/creator.d.ts +13 -16
  113. package/dist/modules/access-model/creator.d.ts.map +1 -1
  114. package/dist/modules/access-model/creator.js.map +1 -1
  115. package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
  116. package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
  117. package/dist/modules/access-model/frontmatter-lines.js +74 -0
  118. package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
  119. package/dist/modules/access-model/plugin-principals.d.ts +5 -3
  120. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -1
  121. package/dist/modules/access-model/plugin-principals.js +11 -17
  122. package/dist/modules/access-model/plugin-principals.js.map +1 -1
  123. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  124. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  125. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  126. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  127. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  128. package/dist/modules/admin/admin-access.service.js +5 -2
  129. package/dist/modules/admin/admin-access.service.js.map +1 -1
  130. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  131. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  132. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  133. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  134. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  135. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  136. package/dist/modules/agent-instructions/compose.js +234 -0
  137. package/dist/modules/agent-instructions/compose.js.map +1 -0
  138. package/dist/modules/agent-instructions/index.d.ts +4 -0
  139. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  140. package/dist/modules/agent-instructions/index.js +4 -0
  141. package/dist/modules/agent-instructions/index.js.map +1 -0
  142. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  143. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  144. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  145. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  146. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  147. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  148. package/dist/modules/auth/account-erasure.service.js +96 -10
  149. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  150. package/dist/modules/auth/account.routes.d.ts +6 -3
  151. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  152. package/dist/modules/auth/account.routes.js +109 -10
  153. package/dist/modules/auth/account.routes.js.map +1 -1
  154. package/dist/modules/auth/auth.routes.d.ts +10 -3
  155. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  156. package/dist/modules/auth/auth.routes.js +9 -4
  157. package/dist/modules/auth/auth.routes.js.map +1 -1
  158. package/dist/modules/auth/auth.service.d.ts +89 -5
  159. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  160. package/dist/modules/auth/auth.service.js +160 -25
  161. package/dist/modules/auth/auth.service.js.map +1 -1
  162. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  163. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  164. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  165. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  166. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  167. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  168. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  169. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  170. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  171. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  172. package/dist/modules/database/advisory-lock.d.ts +103 -0
  173. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  174. package/dist/modules/database/advisory-lock.js +320 -0
  175. package/dist/modules/database/advisory-lock.js.map +1 -0
  176. package/dist/modules/database/connection.d.ts.map +1 -1
  177. package/dist/modules/database/connection.js +9 -1
  178. package/dist/modules/database/connection.js.map +1 -1
  179. package/dist/modules/database/core-schema.d.ts +346 -0
  180. package/dist/modules/database/core-schema.d.ts.map +1 -1
  181. package/dist/modules/database/core-schema.js +119 -0
  182. package/dist/modules/database/core-schema.js.map +1 -1
  183. package/dist/modules/database/migrate.d.ts.map +1 -1
  184. package/dist/modules/database/migrate.js +26 -6
  185. package/dist/modules/database/migrate.js.map +1 -1
  186. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  187. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  188. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  189. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  190. package/dist/modules/diff/diff.routes.js +2 -4
  191. package/dist/modules/diff/diff.routes.js.map +1 -1
  192. package/dist/modules/diff/diff.service.d.ts +4 -1
  193. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  194. package/dist/modules/diff/diff.service.js +79 -81
  195. package/dist/modules/diff/diff.service.js.map +1 -1
  196. package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
  197. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  198. package/dist/modules/kb-fs/bevel-ignore.js +71 -0
  199. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  200. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  201. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  202. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  203. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
  204. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  205. package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
  206. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  207. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  208. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  209. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  210. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  211. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  212. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  213. package/dist/modules/kb-fs/node-fs.js +158 -0
  214. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  215. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  216. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  217. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  218. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  219. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  220. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  221. package/dist/modules/kb-fs/repo-path.js +195 -3
  222. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  223. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  224. package/dist/modules/kb-sync/kb-sync.routes.js +4 -2
  225. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  226. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  227. package/dist/modules/kb-sync/kb-sync.service.js +7 -5
  228. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  229. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -1
  230. package/dist/modules/marketplace/git-http.routes.js +6 -4
  231. package/dist/modules/marketplace/git-http.routes.js.map +1 -1
  232. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +12 -2
  233. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -1
  234. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +32 -3
  235. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -1
  236. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +27 -0
  237. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -1
  238. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +45 -0
  239. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -1
  240. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -1
  241. package/dist/modules/marketplace/github-facade/github-facade.routes.js +30 -6
  242. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -1
  243. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +21 -9
  244. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -1
  245. package/dist/modules/marketplace/github-facade/github-facade.service.js +60 -19
  246. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -1
  247. package/dist/modules/marketplace/github-facade/index.d.ts +1 -1
  248. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -1
  249. package/dist/modules/marketplace/github-facade/index.js +1 -1
  250. package/dist/modules/marketplace/github-facade/index.js.map +1 -1
  251. package/dist/modules/marketplace/marketplace-repo.service.d.ts +14 -1
  252. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -1
  253. package/dist/modules/marketplace/marketplace-repo.service.js +26 -8
  254. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -1
  255. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  256. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  257. package/dist/modules/mcp/downstream-pool.js +221 -0
  258. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  259. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  260. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  261. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  262. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  263. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  264. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  265. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  266. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  267. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  268. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  269. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  270. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  271. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  272. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  273. package/dist/modules/mcp/first-call-probe.js +298 -0
  274. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  275. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  276. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  277. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  278. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  279. package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
  280. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  281. package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
  282. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  283. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  284. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  285. package/dist/modules/mcp/mcp.routes.js +83 -179
  286. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  287. package/dist/modules/mcp/mcp.service.d.ts +218 -61
  288. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  289. package/dist/modules/mcp/mcp.service.js +713 -159
  290. package/dist/modules/mcp/mcp.service.js.map +1 -1
  291. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  292. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -2
  293. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  294. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  295. package/dist/modules/mcp/oauth/oauth-consent.routes.js +3 -1
  296. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  297. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  298. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  299. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  300. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  301. package/dist/modules/plugins/compile/compile-marketplace.d.ts +13 -6
  302. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -1
  303. package/dist/modules/plugins/compile/compile-marketplace.js +119 -36
  304. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -1
  305. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +8 -1
  306. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -1
  307. package/dist/modules/plugins/compile/marketplace-compiler.service.js +9 -6
  308. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -1
  309. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -1
  310. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +32 -9
  311. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -1
  312. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +26 -6
  313. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -1
  314. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +71 -14
  315. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -1
  316. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +4 -2
  317. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -1
  318. package/dist/modules/plugins/discovery/kb-plugin-source.js +9 -6
  319. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -1
  320. package/dist/modules/plugins/discovery/native.source.d.ts +2 -1
  321. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -1
  322. package/dist/modules/plugins/discovery/native.source.js +22 -4
  323. package/dist/modules/plugins/discovery/native.source.js.map +1 -1
  324. package/dist/modules/plugins/discovery/plugin-source.d.ts +17 -3
  325. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -1
  326. package/dist/modules/plugins/index.d.ts +6 -3
  327. package/dist/modules/plugins/index.d.ts.map +1 -1
  328. package/dist/modules/plugins/index.js +6 -3
  329. package/dist/modules/plugins/index.js.map +1 -1
  330. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  331. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  332. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  333. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  334. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  335. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  336. package/dist/modules/plugins/join-request-records.store.js +179 -0
  337. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  338. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  339. package/dist/modules/plugins/join-requests.service.js +4 -2
  340. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  341. package/dist/modules/plugins/plugin-links.d.ts +4 -1
  342. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  343. package/dist/modules/plugins/plugin-links.js +6 -3
  344. package/dist/modules/plugins/plugin-links.js.map +1 -1
  345. package/dist/modules/plugins/plugin-links.service.d.ts +2 -5
  346. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -1
  347. package/dist/modules/plugins/plugin-links.service.js +4 -13
  348. package/dist/modules/plugins/plugin-links.service.js.map +1 -1
  349. package/dist/modules/plugins/plugin-provision.service.d.ts +91 -19
  350. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  351. package/dist/modules/plugins/plugin-provision.service.js +183 -38
  352. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  353. package/dist/modules/plugins/plugin-rename.service.d.ts +3 -1
  354. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -1
  355. package/dist/modules/plugins/plugin-rename.service.js +20 -11
  356. package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
  357. package/dist/modules/plugins/plugins.contract.d.ts +52 -4
  358. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  359. package/dist/modules/plugins/plugins.routes.d.ts +38 -5
  360. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  361. package/dist/modules/plugins/plugins.routes.js +190 -116
  362. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  363. package/dist/modules/plugins/plugins.service.d.ts +38 -6
  364. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  365. package/dist/modules/plugins/plugins.service.js +86 -17
  366. package/dist/modules/plugins/plugins.service.js.map +1 -1
  367. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  368. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  369. package/dist/modules/plugins/plugins.tools.js +82 -0
  370. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  371. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  372. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  373. package/dist/modules/plugins/teams.routes.js +79 -0
  374. package/dist/modules/plugins/teams.routes.js.map +1 -0
  375. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  376. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  377. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  378. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  379. package/dist/modules/secrets-vault/index.d.ts +1 -1
  380. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  381. package/dist/modules/secrets-vault/index.js.map +1 -1
  382. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  383. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  384. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  385. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  386. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  387. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  388. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  389. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  390. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  391. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  392. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  393. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  394. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  395. package/dist/modules/settings/connection-check.d.ts +118 -0
  396. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  397. package/dist/modules/settings/connection-check.js +287 -0
  398. package/dist/modules/settings/connection-check.js.map +1 -0
  399. package/dist/modules/settings/deployment-settings.service.d.ts +153 -7
  400. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  401. package/dist/modules/settings/deployment-settings.service.js +348 -51
  402. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  403. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  404. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  405. package/dist/modules/settings/git-root-folders.js +130 -0
  406. package/dist/modules/settings/git-root-folders.js.map +1 -0
  407. package/dist/modules/settings/oidc-check.d.ts +85 -0
  408. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  409. package/dist/modules/settings/oidc-check.js +172 -0
  410. package/dist/modules/settings/oidc-check.js.map +1 -0
  411. package/dist/modules/settings/setup.routes.d.ts +31 -1
  412. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  413. package/dist/modules/settings/setup.routes.js +400 -108
  414. package/dist/modules/settings/setup.routes.js.map +1 -1
  415. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  416. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  417. package/dist/modules/skills/allowed-tools-check.js +260 -0
  418. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  419. package/dist/modules/skills/index.d.ts +3 -1
  420. package/dist/modules/skills/index.d.ts.map +1 -1
  421. package/dist/modules/skills/index.js +1 -0
  422. package/dist/modules/skills/index.js.map +1 -1
  423. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  424. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  425. package/dist/modules/skills/pending-skills.service.js +18 -70
  426. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  427. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  428. package/dist/modules/skills/skill-access-requests.routes.js +9 -6
  429. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  430. package/dist/modules/skills/skills.contract.d.ts +44 -10
  431. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  432. package/dist/modules/skills/skills.routes.d.ts +8 -1
  433. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  434. package/dist/modules/skills/skills.routes.js +16 -2
  435. package/dist/modules/skills/skills.routes.js.map +1 -1
  436. package/dist/modules/skills/skills.service.d.ts +31 -7
  437. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  438. package/dist/modules/skills/skills.service.js +191 -81
  439. package/dist/modules/skills/skills.service.js.map +1 -1
  440. package/dist/modules/skills/skills.tools.d.ts +4 -1
  441. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  442. package/dist/modules/skills/skills.tools.js +41 -6
  443. package/dist/modules/skills/skills.tools.js.map +1 -1
  444. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  445. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  446. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  447. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  448. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  449. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  450. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  451. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  452. package/dist/modules/tool-auth/external-api-key.interface.d.ts +51 -3
  453. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  454. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  455. package/dist/modules/tool-auth/external-api-key.service.d.ts +16 -6
  456. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  457. package/dist/modules/tool-auth/external-api-key.service.js +61 -18
  458. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  459. package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
  460. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  461. package/dist/modules/tool-auth/internal-token.service.js +5 -2
  462. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  463. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  464. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  465. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  466. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  467. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
  468. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  469. package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
  470. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  471. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  472. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  473. package/dist/modules/tool-helpers/tool-context.js +28 -6
  474. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  475. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  476. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  477. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  478. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  479. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  480. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  481. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  482. package/dist/modules/tool-manuals/index.d.ts +2 -1
  483. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  484. package/dist/modules/tool-manuals/index.js +1 -0
  485. package/dist/modules/tool-manuals/index.js.map +1 -1
  486. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  487. package/dist/modules/tool-manuals/mcp-json-discovery.js +8 -6
  488. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  489. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  490. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  491. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  492. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  493. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  494. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  495. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  496. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  497. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  498. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  499. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  500. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  501. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  502. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  503. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  504. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  505. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  506. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  507. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  508. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  509. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +119 -5
  510. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  511. package/dist/modules/tool-manuals/tool-manuals.service.js +404 -74
  512. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  513. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  514. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  515. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  516. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  517. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  518. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  519. package/dist/modules/workflow/event-bus.d.ts +10 -4
  520. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  521. package/dist/modules/workflow/event-bus.js +14 -11
  522. package/dist/modules/workflow/event-bus.js.map +1 -1
  523. package/dist/modules/workflow/events.routes.d.ts +17 -2
  524. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  525. package/dist/modules/workflow/events.routes.js +31 -12
  526. package/dist/modules/workflow/events.routes.js.map +1 -1
  527. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  528. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  529. package/dist/modules/workflow/file-lock.service.js +27 -4
  530. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  531. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  532. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  533. package/dist/modules/workflow/git/change-request-link.js +34 -0
  534. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  535. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  536. package/dist/modules/workflow/git/git-version.js +3 -1
  537. package/dist/modules/workflow/git/git-version.js.map +1 -1
  538. package/dist/modules/workflow/git/git.service.d.ts +263 -4
  539. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  540. package/dist/modules/workflow/git/git.service.js +528 -118
  541. package/dist/modules/workflow/git/git.service.js.map +1 -1
  542. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  543. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  544. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  545. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  546. package/dist/modules/workflow/git/pull-request.service.d.ts +51 -2
  547. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  548. package/dist/modules/workflow/git/pull-request.service.js +157 -25
  549. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  550. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  551. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  552. package/dist/modules/workflow/pending-commits.service.js +24 -4
  553. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  554. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  555. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  556. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  557. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  558. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  559. package/dist/modules/workflow/recovery-bot.js +3 -1
  560. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  561. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  562. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  563. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  564. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  565. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  566. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  567. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  568. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  569. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  570. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  571. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  572. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  573. package/dist/modules/workflow/sanitize-error.js +8 -4
  574. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  575. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  576. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  577. package/dist/modules/workflow/workflow.routes.js +259 -29
  578. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  579. package/dist/modules/workflow/workflow.service.d.ts +166 -14
  580. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  581. package/dist/modules/workflow/workflow.service.js +906 -106
  582. package/dist/modules/workflow/workflow.service.js.map +1 -1
  583. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  584. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  585. package/dist/modules/workspace/empty-dirs.js +90 -0
  586. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  587. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  588. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  589. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  590. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  591. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  592. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  593. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  594. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  595. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  596. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  597. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  598. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  599. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  600. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  601. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  602. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  603. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  604. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  605. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  606. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  607. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  608. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  609. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  610. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  611. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  612. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  613. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  614. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  615. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  616. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  617. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  618. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  619. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  620. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  621. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  622. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  623. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  624. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  625. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  626. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  627. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  628. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  629. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  630. package/dist/modules/workspace/not-found.d.ts +80 -0
  631. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  632. package/dist/modules/workspace/not-found.js +112 -0
  633. package/dist/modules/workspace/not-found.js.map +1 -0
  634. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  635. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  636. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  637. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  638. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  639. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  640. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  641. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  642. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  643. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  644. package/dist/modules/workspace/startup/kb-git.js +34 -35
  645. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  646. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  647. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  648. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  649. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  650. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  651. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  652. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  653. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  654. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  655. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  656. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  657. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  658. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  659. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  660. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  661. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  662. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  663. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  664. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  665. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  666. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +7 -1
  667. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -1
  668. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +45 -81
  669. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -1
  670. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  671. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  672. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  673. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  674. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  675. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  676. package/dist/modules/workspace/startup/steps/seed-tree.js +154 -78
  677. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  678. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  679. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  680. package/dist/modules/workspace/startup/steps/template-files.step.js +446 -138
  681. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  682. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  683. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  684. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  685. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  686. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  687. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  688. package/dist/modules/workspace/workspace.routes.js +649 -238
  689. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  690. package/dist/modules/workspace/workspace.service.d.ts +290 -12
  691. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  692. package/dist/modules/workspace/workspace.service.js +924 -269
  693. package/dist/modules/workspace/workspace.service.js.map +1 -1
  694. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  695. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  696. package/dist/modules/workspace/workspace.tools.js +1590 -146
  697. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  698. package/dist/modules/workspace/write-denial.d.ts +76 -0
  699. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  700. package/dist/modules/workspace/write-denial.js +138 -0
  701. package/dist/modules/workspace/write-denial.js.map +1 -0
  702. package/dist/shared/canonical-file-identity.d.ts +64 -0
  703. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  704. package/dist/shared/canonical-file-identity.js +92 -0
  705. package/dist/shared/canonical-file-identity.js.map +1 -0
  706. package/dist/shared/domain-errors.d.ts +108 -0
  707. package/dist/shared/domain-errors.d.ts.map +1 -1
  708. package/dist/shared/domain-errors.js +137 -0
  709. package/dist/shared/domain-errors.js.map +1 -1
  710. package/dist/shared/email-identity.d.ts +32 -0
  711. package/dist/shared/email-identity.d.ts.map +1 -0
  712. package/dist/shared/email-identity.js +37 -0
  713. package/dist/shared/email-identity.js.map +1 -0
  714. package/dist/shared/fs.contract.d.ts +206 -0
  715. package/dist/shared/fs.contract.d.ts.map +1 -0
  716. package/dist/shared/fs.contract.js +79 -0
  717. package/dist/shared/fs.contract.js.map +1 -0
  718. package/dist/shared/git-failure.d.ts +80 -0
  719. package/dist/shared/git-failure.d.ts.map +1 -0
  720. package/dist/shared/git-failure.js +139 -0
  721. package/dist/shared/git-failure.js.map +1 -0
  722. package/dist/shared/git-internals.d.ts +17 -0
  723. package/dist/shared/git-internals.d.ts.map +1 -0
  724. package/dist/shared/git-internals.js +123 -0
  725. package/dist/shared/git-internals.js.map +1 -0
  726. package/dist/shared/git.contract.d.ts +157 -0
  727. package/dist/shared/git.contract.d.ts.map +1 -0
  728. package/dist/shared/git.contract.js +103 -0
  729. package/dist/shared/git.contract.js.map +1 -0
  730. package/dist/shared/http-errors.d.ts +33 -0
  731. package/dist/shared/http-errors.d.ts.map +1 -0
  732. package/dist/shared/http-errors.js +35 -0
  733. package/dist/shared/http-errors.js.map +1 -0
  734. package/dist/shared/logger.contract.d.ts +63 -0
  735. package/dist/shared/logger.contract.d.ts.map +1 -0
  736. package/dist/shared/logger.contract.js +126 -0
  737. package/dist/shared/logger.contract.js.map +1 -0
  738. package/dist/shared/logging.d.ts +31 -0
  739. package/dist/shared/logging.d.ts.map +1 -0
  740. package/dist/shared/logging.js +92 -0
  741. package/dist/shared/logging.js.map +1 -0
  742. package/dist/shared/path-containment.d.ts +21 -0
  743. package/dist/shared/path-containment.d.ts.map +1 -0
  744. package/dist/shared/path-containment.js +34 -0
  745. package/dist/shared/path-containment.js.map +1 -0
  746. package/dist/shared/pending-proposals.d.ts +71 -0
  747. package/dist/shared/pending-proposals.d.ts.map +1 -0
  748. package/dist/shared/pending-proposals.js +98 -0
  749. package/dist/shared/pending-proposals.js.map +1 -0
  750. package/dist/shared/printable.d.ts +22 -0
  751. package/dist/shared/printable.d.ts.map +1 -0
  752. package/dist/shared/printable.js +65 -0
  753. package/dist/shared/printable.js.map +1 -0
  754. package/dist/shared/redact-secret.d.ts +30 -0
  755. package/dist/shared/redact-secret.d.ts.map +1 -0
  756. package/dist/shared/redact-secret.js +114 -0
  757. package/dist/shared/redact-secret.js.map +1 -0
  758. package/dist/shared/rename-no-replace.d.ts +119 -0
  759. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  760. package/dist/shared/rename-no-replace.js +323 -0
  761. package/dist/shared/rename-no-replace.js.map +1 -0
  762. package/dist/shared/ssrf.d.ts.map +1 -1
  763. package/dist/shared/ssrf.js +49 -18
  764. package/dist/shared/ssrf.js.map +1 -1
  765. package/dist/version.d.ts +10 -6
  766. package/dist/version.d.ts.map +1 -1
  767. package/dist/version.js +10 -6
  768. package/dist/version.js.map +1 -1
  769. package/kb-template/.bevelignore +4 -3
  770. package/kb-template/AGENTS.md +281 -36
  771. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  772. package/kb-template/access.md +5 -3
  773. package/kb-template/mcp-description.md +27 -0
  774. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  775. package/migrations/0007_github_facade_registered.sql +1 -0
  776. package/migrations/0008_change_request_apply_failure.sql +4 -0
  777. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  778. package/migrations/0010_plugin_join_requests.sql +15 -0
  779. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  780. package/migrations/meta/0006_snapshot.json +1723 -0
  781. package/migrations/meta/0007_snapshot.json +1729 -0
  782. package/migrations/meta/0008_snapshot.json +1753 -0
  783. package/migrations/meta/0009_snapshot.json +1759 -0
  784. package/migrations/meta/0010_snapshot.json +1872 -0
  785. package/migrations/meta/0011_snapshot.json +1884 -0
  786. package/migrations/meta/_journal.json +42 -0
  787. package/package.json +6 -5
  788. package/src/__tests__/core-config.domain.test.ts +67 -2
  789. package/src/__tests__/kb-layout-config.test.ts +186 -1
  790. package/src/__tests__/open-change-gate.ts +15 -0
  791. package/src/core/__tests__/catalog-cache-invalidation.test.ts +83 -3
  792. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  793. package/src/core/__tests__/lifecycle.test.ts +369 -0
  794. package/src/core/__tests__/public-config.test.ts +30 -0
  795. package/src/core/__tests__/readiness.test.ts +109 -0
  796. package/src/core/catalog-cache-invalidation.ts +20 -1
  797. package/src/core/catalog-revision.ts +110 -0
  798. package/src/core/create-core-server.ts +258 -79
  799. package/src/core/create-core-services.ts +302 -81
  800. package/src/core/lifecycle.ts +312 -0
  801. package/src/core/public-config.ts +86 -0
  802. package/src/core/readiness.ts +123 -0
  803. package/src/core-config.ts +70 -4
  804. package/src/index.ts +15 -0
  805. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  806. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  807. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  808. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  809. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  810. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  811. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  812. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +2 -0
  813. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  814. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  815. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  816. package/src/modules/access/__tests__/access-control.service.test.ts +123 -78
  817. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  818. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  819. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  820. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  821. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  822. package/src/modules/access/__tests__/access-plugin-principals.test.ts +18 -1
  823. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  824. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  825. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  826. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  827. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  828. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  829. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  830. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  831. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  832. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  833. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  834. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  835. package/src/modules/access/__tests__/groups-admin.service.test.ts +6 -5
  836. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  837. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  838. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  839. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  840. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  841. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  842. package/src/modules/access/access-control.interface.ts +251 -11
  843. package/src/modules/access/access-control.service.ts +719 -164
  844. package/src/modules/access/access-mutation.service.ts +182 -13
  845. package/src/modules/access/access-view.ts +316 -0
  846. package/src/modules/access/access.routes.ts +293 -95
  847. package/src/modules/access/admin-locked-commit.ts +12 -14
  848. package/src/modules/access/admin-route-helpers.ts +4 -4
  849. package/src/modules/access/change-read-gate.ts +148 -0
  850. package/src/modules/access/creator-access.ts +89 -124
  851. package/src/modules/access/directory-sync-bot.ts +4 -1
  852. package/src/modules/access/groups-admin.service.ts +3 -3
  853. package/src/modules/access/reference-scan.ts +5 -5
  854. package/src/modules/access/roles-admin.service.ts +94 -15
  855. package/src/modules/access/synced-groups-committer.ts +10 -15
  856. package/src/modules/access/synced-groups-writer.ts +3 -12
  857. package/src/modules/access/user-access-removal.service.ts +711 -0
  858. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  859. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  860. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  861. package/src/modules/access-model/access-errors.ts +56 -15
  862. package/src/modules/access-model/access-grammar.ts +85 -48
  863. package/src/modules/access-model/access-splice.ts +18 -18
  864. package/src/modules/access-model/access-template.ts +44 -0
  865. package/src/modules/access-model/change-gate.ts +80 -0
  866. package/src/modules/access-model/creator.ts +18 -21
  867. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  868. package/src/modules/access-model/plugin-principals.ts +12 -19
  869. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  870. package/src/modules/admin/admin-access.service.ts +6 -2
  871. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  872. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  873. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  874. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  875. package/src/modules/agent-instructions/compose.ts +271 -0
  876. package/src/modules/agent-instructions/index.ts +14 -0
  877. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  878. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  879. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  880. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  881. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  882. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  883. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  884. package/src/modules/auth/account-erasure.service.ts +117 -10
  885. package/src/modules/auth/account.routes.ts +115 -14
  886. package/src/modules/auth/auth.routes.ts +20 -7
  887. package/src/modules/auth/auth.service.ts +187 -28
  888. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  889. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  890. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  891. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  892. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  893. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  894. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  895. package/src/modules/database/advisory-lock.ts +346 -0
  896. package/src/modules/database/connection.ts +9 -1
  897. package/src/modules/database/core-schema.ts +123 -0
  898. package/src/modules/database/migrate.ts +27 -6
  899. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  900. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  901. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  902. package/src/modules/diff/diff.routes.ts +2 -4
  903. package/src/modules/diff/diff.service.ts +75 -79
  904. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  905. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  906. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  907. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  908. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  909. package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
  910. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  911. package/src/modules/kb-fs/node-fs.ts +165 -0
  912. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  913. package/src/modules/kb-fs/repo-path.ts +213 -3
  914. package/src/modules/kb-sync/kb-sync.routes.ts +5 -2
  915. package/src/modules/kb-sync/kb-sync.service.ts +8 -5
  916. package/src/modules/marketplace/__tests__/github-facade.test.ts +134 -2
  917. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +2 -1
  918. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  919. package/src/modules/marketplace/git-http.routes.ts +7 -4
  920. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +35 -3
  921. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +55 -0
  922. package/src/modules/marketplace/github-facade/github-facade.routes.ts +32 -6
  923. package/src/modules/marketplace/github-facade/github-facade.service.ts +80 -19
  924. package/src/modules/marketplace/github-facade/index.ts +1 -0
  925. package/src/modules/marketplace/marketplace-repo.service.ts +23 -8
  926. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  927. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  928. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  929. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  930. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  931. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  932. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
  933. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  934. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  935. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  936. package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
  937. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  938. package/src/modules/mcp/downstream-pool.ts +256 -0
  939. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  940. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  941. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  942. package/src/modules/mcp/first-call-probe.ts +418 -0
  943. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  944. package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
  945. package/src/modules/mcp/mcp.routes.ts +376 -482
  946. package/src/modules/mcp/mcp.service.ts +846 -176
  947. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +5 -2
  948. package/src/modules/mcp/oauth/oauth-consent.routes.ts +4 -4
  949. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  950. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +10 -7
  951. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  952. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +139 -10
  953. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  954. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  955. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  956. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +148 -7
  957. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +10 -13
  958. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  959. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +129 -19
  960. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +91 -29
  961. package/src/modules/plugins/__tests__/plugins.routes.test.ts +370 -19
  962. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  963. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  964. package/src/modules/plugins/compile/compile-marketplace.ts +143 -46
  965. package/src/modules/plugins/compile/marketplace-compiler.service.ts +8 -7
  966. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +56 -20
  967. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +30 -9
  968. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +93 -18
  969. package/src/modules/plugins/discovery/kb-plugin-source.ts +10 -7
  970. package/src/modules/plugins/discovery/native.source.ts +21 -3
  971. package/src/modules/plugins/discovery/plugin-source.ts +17 -3
  972. package/src/modules/plugins/index.ts +16 -3
  973. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  974. package/src/modules/plugins/join-request-records.store.ts +353 -0
  975. package/src/modules/plugins/join-requests.service.ts +7 -4
  976. package/src/modules/plugins/plugin-links.service.ts +4 -17
  977. package/src/modules/plugins/plugin-links.ts +6 -3
  978. package/src/modules/plugins/plugin-provision.service.ts +209 -37
  979. package/src/modules/plugins/plugin-rename.service.ts +21 -9
  980. package/src/modules/plugins/plugins.contract.ts +52 -4
  981. package/src/modules/plugins/plugins.routes.ts +204 -124
  982. package/src/modules/plugins/plugins.service.ts +92 -18
  983. package/src/modules/plugins/plugins.tools.ts +88 -0
  984. package/src/modules/plugins/teams.routes.ts +129 -0
  985. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  986. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  987. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  988. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  989. package/src/modules/secrets-vault/index.ts +1 -0
  990. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  991. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  992. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  993. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  994. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  995. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +188 -6
  996. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  997. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  998. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  999. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1000. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1001. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1002. package/src/modules/settings/connection-check.ts +351 -0
  1003. package/src/modules/settings/deployment-settings.service.ts +403 -57
  1004. package/src/modules/settings/git-root-folders.ts +171 -0
  1005. package/src/modules/settings/oidc-check.ts +229 -0
  1006. package/src/modules/settings/setup.routes.ts +456 -110
  1007. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1008. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1009. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1010. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1011. package/src/modules/skills/__tests__/skills.service.test.ts +223 -2
  1012. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1013. package/src/modules/skills/index.ts +4 -0
  1014. package/src/modules/skills/pending-skills.service.ts +21 -82
  1015. package/src/modules/skills/skill-access-requests.routes.ts +10 -6
  1016. package/src/modules/skills/skills.contract.ts +43 -11
  1017. package/src/modules/skills/skills.routes.ts +16 -1
  1018. package/src/modules/skills/skills.service.ts +214 -84
  1019. package/src/modules/skills/skills.tools.ts +43 -5
  1020. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1021. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1022. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
  1023. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1024. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1025. package/src/modules/tool-auth/external-api-key.interface.ts +56 -3
  1026. package/src/modules/tool-auth/external-api-key.service.ts +73 -25
  1027. package/src/modules/tool-auth/internal-token.service.ts +5 -2
  1028. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1029. package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
  1030. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1031. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1032. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1033. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1034. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1035. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1036. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1037. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1038. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1039. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1040. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +9 -4
  1041. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1042. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1043. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1044. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1045. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1046. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1047. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1048. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1049. package/src/modules/tool-manuals/index.ts +3 -0
  1050. package/src/modules/tool-manuals/mcp-json-discovery.ts +13 -10
  1051. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1052. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1053. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1054. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1055. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1056. package/src/modules/tool-manuals/tool-manuals.service.ts +432 -87
  1057. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1058. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1059. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1060. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1061. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1062. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1063. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1064. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1065. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +2 -0
  1066. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1067. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1068. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1069. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1070. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1071. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +2 -0
  1072. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1073. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +294 -25
  1074. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1075. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1076. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1077. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +14 -0
  1078. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +5 -0
  1079. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1080. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1081. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1082. package/src/modules/workflow/event-bus.ts +26 -28
  1083. package/src/modules/workflow/events.routes.ts +56 -21
  1084. package/src/modules/workflow/file-lock.service.ts +27 -4
  1085. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1086. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1087. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1088. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1089. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1090. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1091. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1092. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1093. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1094. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1095. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1096. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1097. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1098. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1099. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1100. package/src/modules/workflow/git/git-version.ts +5 -2
  1101. package/src/modules/workflow/git/git.service.ts +684 -144
  1102. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1103. package/src/modules/workflow/git/pull-request.service.ts +179 -27
  1104. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1105. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1106. package/src/modules/workflow/recovery-bot.ts +4 -1
  1107. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1108. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1109. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1110. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1111. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1112. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1113. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1114. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1115. package/src/modules/workflow/sanitize-error.ts +9 -4
  1116. package/src/modules/workflow/workflow.routes.ts +273 -29
  1117. package/src/modules/workflow/workflow.service.ts +1087 -171
  1118. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1119. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1120. package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
  1121. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1122. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1123. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1124. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1125. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1126. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1127. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1128. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1129. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1130. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +25 -6
  1131. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1132. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -5
  1133. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1134. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +56 -1
  1135. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1136. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1137. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1138. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1139. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1140. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1141. package/src/modules/workspace/empty-dirs.ts +79 -0
  1142. package/src/modules/workspace/file-readers/README.md +32 -0
  1143. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1144. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1145. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1146. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1147. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1148. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1149. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1150. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1151. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1152. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1153. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1154. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1155. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1156. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1157. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1158. package/src/modules/workspace/not-found.ts +119 -0
  1159. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1160. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1161. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1162. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1163. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1164. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1165. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1166. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1215 -73
  1167. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1168. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1169. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1170. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1171. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +46 -85
  1172. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1173. package/src/modules/workspace/startup/steps/seed-tree.ts +159 -78
  1174. package/src/modules/workspace/startup/steps/template-files.step.ts +492 -141
  1175. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1176. package/src/modules/workspace/workspace.routes.ts +687 -257
  1177. package/src/modules/workspace/workspace.service.ts +991 -303
  1178. package/src/modules/workspace/workspace.tools.ts +1787 -180
  1179. package/src/modules/workspace/write-denial.ts +189 -0
  1180. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1181. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1182. package/src/shared/__tests__/filename.test.ts +0 -0
  1183. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1184. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1185. package/src/shared/__tests__/git-internals.test.ts +115 -0
  1186. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1187. package/src/shared/__tests__/logging.test.ts +185 -0
  1188. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1189. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1190. package/src/shared/__tests__/printable.test.ts +64 -0
  1191. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1192. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1193. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1194. package/src/shared/canonical-file-identity.ts +92 -0
  1195. package/src/shared/domain-errors.ts +149 -0
  1196. package/src/shared/email-identity.ts +38 -0
  1197. package/src/shared/fs.contract.ts +225 -0
  1198. package/src/shared/git-failure.ts +196 -0
  1199. package/src/shared/git-internals.ts +124 -0
  1200. package/src/shared/git.contract.ts +170 -0
  1201. package/src/shared/http-errors.ts +36 -0
  1202. package/src/shared/logger.contract.ts +141 -0
  1203. package/src/shared/logging.ts +96 -0
  1204. package/src/shared/path-containment.ts +34 -0
  1205. package/src/shared/pending-proposals.ts +161 -0
  1206. package/src/shared/printable.ts +68 -0
  1207. package/src/shared/redact-secret.ts +118 -0
  1208. package/src/shared/rename-no-replace.ts +386 -0
  1209. package/src/shared/ssrf.ts +44 -16
  1210. package/src/version.ts +10 -6
  1211. package/src/modules/diff/diff-paths.ts +0 -9
  1212. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1213. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1214. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1215. package/src/shared/__tests__/fs-walk.test.ts +0 -63
  1216. package/src/shared/__tests__/kb-walk.test.ts +0 -107
  1217. package/src/shared/fs-errors.ts +0 -10
  1218. package/src/shared/fs-walk.ts +0 -45
  1219. package/src/shared/hash-email.ts +0 -14
  1220. package/src/shared/kb-walk.ts +0 -95
@@ -1,24 +1,33 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { execFile, spawn } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
- import { createHash } from 'node:crypto';
3
+ import { logger } from '../../shared/logging.js';
6
4
 
7
- import { isAbsence } from '../../shared/fs-errors.js';
8
- import { walkKb, type KbWalkListener } from '../../shared/kb-walk.js';
5
+ const log = logger('access');
6
+
7
+ import { isAbsence, type ITreeWalker, type WalkListener } from '../../shared/fs.contract.js';
8
+ import { isGitTimeout, type IGitRunner } from '../../shared/git.contract.js';
9
+ import { hashEmail } from '../../shared/email-identity.js';
10
+ import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
9
11
  import type { WorkspaceService } from '../workspace/workspace.service.js';
10
12
  import type {
11
13
  IAccessControl,
14
+ AccessDecision,
15
+ AccessDecisionSource,
12
16
  AccessTargetKind,
17
+ DenialSources,
13
18
  GrantPrincipal,
14
19
  GrantSource,
15
20
  GrantSources,
21
+ PathHolders,
22
+ ProspectiveHolders,
16
23
  ResolvedPrincipal,
17
24
  } from './access-control.interface.js';
18
25
  import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, isPersonalPluginDir,
19
26
  pluginIdentityOf,
27
+ platformRestoreDestination,
20
28
  } from '@bevel-software/platform-shared';
21
29
  import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
30
+ import { WorkflowDomainError } from '../../shared/domain-errors.js';
22
31
  import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
23
32
  import {
24
33
  GROUPS_YAML,
@@ -40,6 +49,7 @@ import {
40
49
  parseAccessFile,
41
50
  parseOwnAccessEntries,
42
51
  parseRolesYaml,
52
+ requiredVerbsFor,
43
53
  sourceVerbsFor,
44
54
  type AccessFile,
45
55
  type OwnEntries,
@@ -52,8 +62,6 @@ import {
52
62
  DIRECTORY_SYNC_BOT_NAME,
53
63
  } from './directory-sync-bot.js';
54
64
 
55
- const execFileAsync = promisify(execFile);
56
-
57
65
  /**
58
66
  * Read many objects from a git repo in ONE `git cat-file --batch` process.
59
67
  * `specs` are `<ref>:<path>` lines; the result array is index-aligned with
@@ -62,60 +70,39 @@ const execFileAsync = promisify(execFile);
62
70
  * request: `<oid> <type> <size>\n<size bytes>\n`, or `<spec> missing\n`
63
71
  * (the spec may itself contain spaces, hence the endsWith checks).
64
72
  */
65
- function catFileBatch(repoDir: string, specs: string[]): Promise<(string | null)[]> {
66
- return new Promise((resolve, reject) => {
67
- const child = spawn('git', ['-C', repoDir, 'cat-file', '--batch'], {
68
- stdio: ['pipe', 'pipe', 'pipe'],
69
- });
70
- const chunks: Buffer[] = [];
71
- const errChunks: Buffer[] = [];
72
- child.stdout.on('data', (c: Buffer) => chunks.push(c));
73
- child.stderr.on('data', (c: Buffer) => errChunks.push(c));
74
- // A dying git can close stdin mid-write; the 'close' handler below still
75
- // fires with the exit code, which is the error we want to surface.
76
- child.stdin.on('error', () => undefined);
77
- child.on('error', reject);
78
- child.on('close', (code) => {
79
- if (code !== 0) {
80
- reject(new Error(`git cat-file --batch exited ${code}: ${Buffer.concat(errChunks).toString('utf-8').trim()}`));
81
- return;
82
- }
83
- try {
84
- const out = Buffer.concat(chunks);
85
- const results: (string | null)[] = [];
86
- let off = 0;
87
- for (let i = 0; i < specs.length; i++) {
88
- const nl = out.indexOf(0x0a, off);
89
- if (nl < 0) throw new Error('unexpected end of git cat-file output');
90
- const header = out.subarray(off, nl).toString('utf-8');
91
- off = nl + 1;
92
- if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
93
- results.push(null);
94
- continue;
95
- }
96
- const parts = header.split(' ');
97
- const size = Number(parts[2]);
98
- if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
99
- throw new Error(`unexpected git cat-file header: ${header}`);
100
- }
101
- // Non-blob (a directory path resolves to a tree) → null, but the
102
- // payload still has to be skipped to stay aligned.
103
- results.push(parts[1] === 'blob' ? out.subarray(off, off + size).toString('utf-8') : null);
104
- off += size + 1; // payload + trailing LF
105
- }
106
- resolve(results);
107
- } catch (err) {
108
- reject(err);
109
- }
110
- });
111
- child.stdin.write(`${specs.join('\n')}\n`);
112
- child.stdin.end();
73
+ async function catFileBatch(
74
+ runner: IGitRunner,
75
+ repoDir: string,
76
+ specs: string[],
77
+ ): Promise<(string | null)[]> {
78
+ // Bytes, not text: every `<size>` in the header counts bytes, and the walk
79
+ // below has to step by that count through content that is not all ASCII.
80
+ const { stdout: out } = await runner.run(repoDir, ['cat-file', '--batch'], {
81
+ input: `${specs.join('\n')}\n`,
82
+ encoding: 'buffer',
113
83
  });
114
- }
115
-
116
- /** Mirrors `shared/hash-email.ts`. Duplicated to avoid a cross-cutting import. */
117
- function sha256Email(email: string): string {
118
- return createHash('sha256').update(email.trim().toLowerCase()).digest('hex');
84
+ const results: (string | null)[] = [];
85
+ let off = 0;
86
+ for (let i = 0; i < specs.length; i++) {
87
+ const nl = out.indexOf(0x0a, off);
88
+ if (nl < 0) throw new Error('unexpected end of git cat-file output');
89
+ const header = out.subarray(off, nl).toString('utf-8');
90
+ off = nl + 1;
91
+ if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
92
+ results.push(null);
93
+ continue;
94
+ }
95
+ const parts = header.split(' ');
96
+ const size = Number(parts[2]);
97
+ if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
98
+ throw new Error(`unexpected git cat-file header: ${header}`);
99
+ }
100
+ // Non-blob (a directory path resolves to a tree) → null, but the
101
+ // payload still has to be skipped to stay aligned.
102
+ results.push(parts[1] === 'blob' ? out.subarray(off, off + size).toString('utf-8') : null);
103
+ off += size + 1; // payload + trailing LF
104
+ }
105
+ return results;
119
106
  }
120
107
 
121
108
 
@@ -133,10 +120,11 @@ interface AccessModel {
133
120
  * Canonical emails that count as Admin whatever `roles.yaml` says — the
134
121
  * deployment owner (`ADMIN_EMAIL`). Empty when none is configured.
135
122
  *
136
- * This exists for the two hardcoded `write` rescues below, and only those.
137
- * It is NOT a general grant: it never enters scope resolution, so it gives
138
- * no read, no download, and no write anywhere except `roles.yaml` and
139
- * `access.md`. The deployment owner is the person who can already change
123
+ * This exists for the two hardcoded `write` rescues below and the root write
124
+ * floor, and only those. It is NOT a general grant: it never enters scope
125
+ * resolution, so it gives no read, no download, and no write beyond
126
+ * `roles.yaml`, `access.md`, and what the root floor reaches (see
127
+ * `hasPermissionResolved`). The deployment owner is the person who can already change
140
128
  * `ADMIN_EMAIL` itself, so admitting them to the rescue path concedes
141
129
  * nothing they could not already take — while withholding it turns a
142
130
  * `roles.yaml` that has lost its last Admin into a knowledge base nobody
@@ -160,7 +148,7 @@ function roleKnown(roles: RolesIndex, canonicalRole: string): boolean {
160
148
  * nothing, bare grant tokens fall through to roles, and group-backed denies
161
149
  * drop (the owner's explicit degrade-loudly decision, NOT fail-closed). The
162
150
  * marker is carried on the loaded model and surfaced by the groups admin
163
- * endpoints so the Groups page can banner it.
151
+ * endpoints so the Groups & Members page can banner it.
164
152
  */
165
153
  export type GroupsHealth = { ok: true } | { ok: false; file: string; reason: string };
166
154
 
@@ -171,13 +159,13 @@ export type GroupsHealth = { ok: true } | { ok: false; file: string; reason: str
171
159
  * even when the synced file is empty or malformed — falling back would
172
160
  * resurrect retired manual groups); otherwise `groups.yaml` → manual mode.
173
161
  *
174
- * `read` returns null for a genuinely-absent file and may THROW for any other
175
- * failure. Only ENOENT/ENOTDIR count as absent (the caller may also whitelist
176
- * them into null); every other read error — notably a non-absence error on
177
- * `synced-groups.yaml` — is treated as a BROKEN source: no groups, no
178
- * fallback to the manual file (falling back would resurrect retired groups),
179
- * and an `ok: false` health marker. Structural parse failures degrade the
180
- * same way; this function never throws.
162
+ * `read` returns null for a genuinely-absent file — ENOENT/ENOTDIR, the
163
+ * reader's judgement through the fs probe — and THROWS for any other failure.
164
+ * Every thrown error — notably one on `synced-groups.yaml` — is treated as a
165
+ * BROKEN source: no groups, no fallback to the manual file (falling back
166
+ * would resurrect retired groups), and an `ok: false` health marker.
167
+ * Structural parse failures degrade the same way; this function never throws
168
+ * (except to let an `AccessUnreadableError` through — see below).
181
169
  */
182
170
  export async function loadActiveGroups(
183
171
  read: (filename: string) => Promise<string | null>,
@@ -201,14 +189,10 @@ export async function loadActiveGroups(
201
189
  // subprocess. roles.yaml and access.md already fail the build closed on
202
190
  // the same error; groups cannot be the one input that does not.
203
191
  if (err instanceof AccessUnreadableError) throw err;
204
- if (isAbsence(err)) {
205
- syncedText = null;
206
- } else {
207
- // A non-absence read error on the SYNCED source must NOT fall back to
208
- // groups.yaml — the synced file may exist and its manual predecessor is
209
- // retired. Broken-groups instead.
210
- return broken(SYNCED_GROUPS_YAML, err instanceof Error ? err.message : String(err));
211
- }
192
+ // A read error on the SYNCED source must NOT fall back to groups.yaml —
193
+ // the synced file may exist and its manual predecessor is retired.
194
+ // Broken-groups instead.
195
+ return broken(SYNCED_GROUPS_YAML, err instanceof Error ? err.message : String(err));
212
196
  }
213
197
 
214
198
  const sourceFile = syncedText !== null ? SYNCED_GROUPS_YAML : GROUPS_YAML;
@@ -220,8 +204,7 @@ export async function loadActiveGroups(
220
204
  text = await read(GROUPS_YAML);
221
205
  } catch (err) {
222
206
  if (err instanceof AccessUnreadableError) throw err; // see above
223
- if (isAbsence(err)) text = null;
224
- else return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
207
+ return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
225
208
  }
226
209
  }
227
210
  if (text === null) return { groups: new Map(), sourceFile, warnings: [], health: { ok: true } };
@@ -270,6 +253,15 @@ interface AccessScope {
270
253
  byEmail: Map<string, GrantState>;
271
254
  everyone: GrantState | undefined;
272
255
  source: ScopeSource;
256
+ /**
257
+ * The keys whose `'denied'` in `byRole` / `byEmail` is IMPLIED — a denial
258
+ * of a verb the target presupposes (`deny write` when resolving `owner`),
259
+ * with no `deny <target>` line of their own here. The verdict is as real as
260
+ * any (the effective walk, the eligible lists and the collapsed view treat
261
+ * it as denied), but it is not a LINE in this file: `denialSources`, which
262
+ * reports the entries a person can lift, leaves it out.
263
+ */
264
+ implied: { byRole: Set<string>; byEmail: Set<string> };
273
265
  }
274
266
 
275
267
  /**
@@ -301,7 +293,7 @@ function accessFileListener(
301
293
  repoDir: string,
302
294
  out: Map<string, AccessFile>,
303
295
  pluginDirs: Map<string, string>,
304
- ): KbWalkListener {
296
+ ): WalkListener {
305
297
  return {
306
298
  async onFile(dir, name) {
307
299
  const rel = dir ? `${dir}/${name}` : name;
@@ -316,14 +308,12 @@ function accessFileListener(
316
308
  if (!parsed.ok) {
317
309
  // Treat as if the file didn't exist for resolution purposes.
318
310
  // Admin-rescue on access.md paths still lets an admin fix it.
319
- for (const e of parsed.errors) console.warn(`[access] ${e} — file ignored`);
311
+ for (const e of parsed.errors) log.warn(`${e} — file ignored`);
320
312
  return;
321
313
  }
322
- for (const w of parsed.warnings) console.warn(`[access] ${w}`);
314
+ for (const w of parsed.warnings) log.warn(w);
323
315
  if (out.has(parsed.file.dir)) {
324
- console.warn(
325
- `[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`,
326
- );
316
+ log.warn(`${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
327
317
  return;
328
318
  }
329
319
  out.set(parsed.file.dir, parsed.file);
@@ -351,13 +341,21 @@ function resolveScopes(
351
341
  fileOwn?: OwnEntries | null,
352
342
  ): AccessScope[] {
353
343
  const target = verb;
344
+ // Both lists come from the grammar's one dependency graph (`VERB_REQUIRES`):
345
+ // the verbs whose grant confers the target, and the verbs the target
346
+ // presupposes, whose denial strips it.
354
347
  const supersets = sourceVerbsFor(verb).filter((v) => v !== verb);
348
+ const prerequisites = requiredVerbsFor(verb).filter((v) => v !== verb);
355
349
 
356
350
  // Build one scope's effective verdicts. The target verb contributes both
357
- // grants and denials; superset verbs contribute grants only (a `deny write`
358
- // never strips `read`). Within the scope a grant always wins over a deny of
359
- // the same principal, so a same-file superset grant overrides a target deny
360
- // (e.g. `owner:` beats `deny write`, `write:` beats `deny read`).
351
+ // grants and denials. Superset verbs contribute grants only (a `deny write`
352
+ // never strips `read`); prerequisite verbs contribute DENIALS only (a `deny
353
+ // write` strips `owner`, a `deny read` strips all three — nobody owns what
354
+ // they may not edit, or edits what they may not open), and a `read` grant
355
+ // confers no `write`. Within the scope a grant always wins over a deny of
356
+ // the same principal, so a same-file grant of the target or of a superset
357
+ // overrides a deny of the target or of a prerequisite (`owner:` beats `deny
358
+ // write`, `write:` beats `deny read`).
361
359
  const buildScope = (
362
360
  entries: Record<Verb, ParsedEntry[]>,
363
361
  filterRoles: boolean,
@@ -365,23 +363,39 @@ function resolveScopes(
365
363
  ): AccessScope => {
366
364
  const byRole = new Map<string, GrantState>();
367
365
  const byEmail = new Map<string, GrantState>();
366
+ const implied = { byRole: new Set<string>(), byEmail: new Set<string>() };
368
367
  const set = (entry: ParsedEntry, state: GrantState) => {
369
368
  const map = entry.kind === 'role' ? byRole : byEmail;
370
369
  const key = entry.kind === 'role' ? entry.role : entry.email;
371
370
  if (map.get(key) === 'grant') return; // a grant in this scope sticks
372
371
  map.set(key, state);
373
372
  };
373
+ const skip = (entry: ParsedEntry) =>
374
+ filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role);
374
375
  for (const entry of entries[target]) {
375
- if (filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role)) continue;
376
+ if (skip(entry)) continue;
376
377
  set(entry, entry.deny ? 'denied' : 'grant');
377
378
  }
378
379
  for (const src of supersets) {
379
380
  for (const entry of entries[src]) {
380
381
  if (entry.deny) continue; // grant-only fold
381
- if (filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role)) continue;
382
+ if (skip(entry)) continue;
382
383
  set(entry, 'grant');
383
384
  }
384
385
  }
386
+ // Last, so a target or superset GRANT already in the map stands, and a
387
+ // target's own `deny` line is never re-labelled as implied.
388
+ for (const src of prerequisites) {
389
+ for (const entry of entries[src]) {
390
+ if (!entry.deny) continue; // deny-only fold
391
+ if (skip(entry)) continue;
392
+ const map = entry.kind === 'role' ? byRole : byEmail;
393
+ const key = entry.kind === 'role' ? entry.role : entry.email;
394
+ if (map.has(key)) continue; // an own verdict (grant or deny) is already the answer
395
+ map.set(key, 'denied');
396
+ (entry.kind === 'role' ? implied.byRole : implied.byEmail).add(key);
397
+ }
398
+ }
385
399
  // The anonymous caller's verdict at this scope — what someone who holds
386
400
  // exactly the PUBLIC keys (a plugin principal whose plugin admits
387
401
  // everyone, so everyone holds it) resolves to, by the same tiers a named
@@ -404,7 +418,7 @@ function resolveScopes(
404
418
  : publicDeny
405
419
  ? 'denied'
406
420
  : byRole.get(EVERYONE_CANONICAL);
407
- return { byRole, byEmail, everyone, source };
421
+ return { byRole, byEmail, everyone, source, implied };
408
422
  };
409
423
 
410
424
  const scopes: AccessScope[] = [];
@@ -463,13 +477,105 @@ function principalKeysOf(model: AccessModel, email: string): Set<string> | undef
463
477
  return new Set([...(own ?? []), ...pub]);
464
478
  }
465
479
 
480
+ /** The explicit key of the Admin ROLE — never the bare token, which a same-named group may own. */
481
+ const ADMIN_ROLE_KEY = `${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`;
482
+
483
+ /**
484
+ * Add to `keys` what holding them confers on top: every plugin principal
485
+ * expanded from one of the keys, and the public keys every caller holds.
486
+ */
487
+ function addDerivedKeys(model: AccessModel, keys: Set<string>): Set<string> {
488
+ for (const [key, principal] of model.roles.byCanonical) {
489
+ if (principal.kind !== 'plugin' || !principal.sourceKeys) continue;
490
+ for (const source of principal.sourceKeys) {
491
+ if (keys.has(source)) {
492
+ keys.add(key);
493
+ break;
494
+ }
495
+ }
496
+ }
497
+ for (const key of model.roles.publicKeys ?? []) keys.add(key);
498
+ return keys;
499
+ }
500
+
501
+ /**
502
+ * Every principal key that being in `group` confers — the group's OWN key,
503
+ * the roles that list the group (`group:<Name>` in roles.yaml), the plugin
504
+ * principals whose roster admits the group directly or through one of those
505
+ * roles, and the public keys every caller holds. This is the group-shaped
506
+ * twin of `principalKeysOf`: a member's key set minus everything they hold
507
+ * for reasons of their own. `null` when no group of that name exists.
508
+ *
509
+ * Groups are keyed by their bare canonical name (`mergeGroupsIntoRoles`);
510
+ * roles by `role/<name>`. A bare key whose record is a ROLE is not a group,
511
+ * however it was spelled.
512
+ */
513
+ function principalKeysOfGroup(model: AccessModel, group: string): Set<string> | null {
514
+ const canonical = canonicalRoleName(group);
515
+ const record = model.roles.byCanonical.get(canonical);
516
+ if (!record || record.kind !== 'group') return null;
517
+ const keys = new Set<string>([canonical]);
518
+ for (const [key, principal] of model.roles.byCanonical) {
519
+ if (principal.kind === 'role' && principal.groupRefs?.has(canonical)) keys.add(key);
520
+ }
521
+ // Plugin principals were expanded from role/group entries; a plugin that
522
+ // admits the group, or a role the group is folded into, admits the group.
523
+ return addDerivedKeys(model, keys);
524
+ }
525
+
526
+ /** The key set of a caller who holds nothing: `hasPermissionForKeys` then answers as `everyone`. */
527
+ const NO_KEYS: ReadonlySet<string> = new Set();
528
+
529
+ /**
530
+ * The tier-2 half of `hasPermissionResolved` on its own: a verdict for a set
531
+ * of principal KEYS with no person behind them — no direct-email tier, no
532
+ * admin rescue, no machine-owned rule — only the Admin write floor, when the
533
+ * keys include the Admin role. Closest scope first; within a scope a
534
+ * grant to any key wins over a denial to another, exactly as for a person.
535
+ */
536
+ function hasPermissionForKeys(
537
+ model: AccessModel,
538
+ verb: Verb,
539
+ keys: ReadonlySet<string>,
540
+ relativePath: string,
541
+ fileOwn?: OwnEntries | null,
542
+ ): boolean {
543
+ const adminFloor = verb === 'write' && keys.has(ADMIN_ROLE_KEY);
544
+ for (const scope of resolveScopes(model, verb, relativePath, fileOwn)) {
545
+ if (adminFloor && isAdminFloorScope(scope, relativePath)) return true;
546
+ let grant = false;
547
+ let deny = false;
548
+ for (const key of keys) {
549
+ const state = scope.byRole.get(key);
550
+ if (state === 'denied') deny = true;
551
+ else if (state === 'grant') grant = true;
552
+ }
553
+ if (grant) return true;
554
+ if (deny) return false;
555
+ if (scope.everyone) return scope.everyone === 'grant';
556
+ }
557
+ return adminFloor;
558
+ }
559
+
560
+ /**
561
+ * Whether a scope sits at the Admin write floor: the repository root's own
562
+ * `access.md`, or the own frontmatter of a file directly in the root (those
563
+ * files are part of the root, so their own rules cannot take Admin's write
564
+ * away either). A folder directly in the root is not covered — its own
565
+ * `access.md` is an ordinary subfolder scope that may exclude Admin.
566
+ */
567
+ function isAdminFloorScope(scope: AccessScope, relativePath: string): boolean {
568
+ if (scope.source.kind === 'own') return !relativePath.includes('/');
569
+ return scope.source.path === 'access.md';
570
+ }
571
+
466
572
  function isAdminEmail(model: AccessModel, email: string): boolean {
467
573
  if (model.deploymentOwners.has(email)) return true;
468
574
  const roles = model.roles.byEmail.get(email);
469
575
  // Check the explicit `role/admin` alias, NOT the bare token: bare-name
470
576
  // precedence is group-first, so a group that happens to be named "Admin"
471
577
  // owns the bare key — and its members must never inherit the capability.
472
- return !!roles && roles.has(`${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`);
578
+ return !!roles && roles.has(ADMIN_ROLE_KEY);
473
579
  }
474
580
 
475
581
  /**
@@ -503,6 +609,16 @@ function isAdminEmail(model: AccessModel, email: string): boolean {
503
609
  * owner is whoever can already set `ADMIN_EMAIL`, so this concedes no
504
610
  * authority they did not have; it only gives it a door.
505
611
  *
612
+ * One floor for `write`, applied during the walk: an admin (same definition)
613
+ * always holds `write` at the repository root. When the walk reaches the root
614
+ * `access.md` (or the own frontmatter of a file directly in the root) — or runs
615
+ * out of scopes without a verdict — an admin is granted,
616
+ * whatever the root file says, so no root rule can lock the deployment out of
617
+ * its own tree. A NEARER scope still decides first: a subfolder that denies
618
+ * Admin (or denies `everyone` without naming Admin) excludes admins there like
619
+ * anyone else. The root file's own entries are not rewritten — the floor is a
620
+ * verdict, not a grant line — so `grantSources` stays file-backed.
621
+ *
506
622
  * No special-cases for `read`/`download` — they fall through to scope resolution.
507
623
  */
508
624
  function hasPermissionResolved(
@@ -512,44 +628,88 @@ function hasPermissionResolved(
512
628
  relativePath: string,
513
629
  fileOwn?: OwnEntries | null,
514
630
  ): boolean {
631
+ return decidePermissionResolved(model, verb, userEmail, relativePath, fileOwn).allowed;
632
+ }
633
+
634
+ /**
635
+ * WHAT decided one caller's verdict: `hasPermissionResolved` with its reasoning
636
+ * kept. `scope` is the scope whose entry settled it (null for the hardcoded
637
+ * admin rescues and for default-deny; the root scope, or null when there is
638
+ * none, for the Admin write floor); `via` is the tier that matched and `key`
639
+ * the principal key it matched on (the caller's email, a role/group/plugin
640
+ * key, or `everyone`). `hasPermissionResolved` IS this function's `allowed`,
641
+ * so an explanation can never disagree with the gate.
642
+ */
643
+ interface PermissionDecision {
644
+ allowed: boolean;
645
+ scope: AccessScope | null;
646
+ via: 'email' | 'principal' | 'everyone' | 'admin-rescue' | 'admin-floor' | 'default-deny';
647
+ key?: string;
648
+ }
649
+
650
+ function decidePermissionResolved(
651
+ model: AccessModel,
652
+ verb: Verb,
653
+ userEmail: string,
654
+ relativePath: string,
655
+ fileOwn?: OwnEntries | null,
656
+ ): PermissionDecision {
515
657
  const email = canonicalEmail(userEmail);
516
658
 
517
659
  if (verb === 'write') {
518
- if (relativePath === 'roles.yaml') return isAdminEmail(model, email);
519
- if (isAccessMdPath(relativePath) && isAdminEmail(model, email)) return true;
660
+ if (relativePath === 'roles.yaml') {
661
+ return { allowed: isAdminEmail(model, email), scope: null, via: 'admin-rescue' };
662
+ }
663
+ if (isAccessMdPath(relativePath) && isAdminEmail(model, email)) {
664
+ return { allowed: true, scope: null, via: 'admin-rescue' };
665
+ }
520
666
  }
521
667
 
522
- const userRoles = principalKeysOf(model, email);
668
+ const adminFloor = verb === 'write' && isAdminEmail(model, email);
669
+ // For write, the deployment owner IS Admin — the floor admits them, so the
670
+ // Admin rules along the way bind them too: a subfolder denying Admin excludes
671
+ // the owner exactly as it excludes the role's members.
672
+ let userRoles = principalKeysOf(model, email);
673
+ if (adminFloor && !userRoles?.has(ADMIN_ROLE_KEY)) {
674
+ userRoles = new Set([...(userRoles ?? []), ...principalKeysOfToken(model, ADMIN_ROLE_KEY)]);
675
+ }
523
676
  const scopes = resolveScopes(model, verb, relativePath, fileOwn);
524
677
 
525
678
  for (const scope of scopes) {
679
+ if (adminFloor && isAdminFloorScope(scope, relativePath)) return { allowed: true, scope, via: 'admin-floor' };
680
+
526
681
  // Tier 1 — direct email entry is the most specific verdict at this scope.
527
682
  const direct = scope.byEmail.get(email);
528
- if (direct) return direct === 'grant';
683
+ if (direct) return { allowed: direct === 'grant', scope, via: 'email', key: email };
529
684
 
530
685
  // Tier 2 — the caller's roles. A grant via any one of them wins over a deny
531
686
  // via another at this same scope (a `deny Engineer` doesn't undo an
532
687
  // unrelated `Admin` grant).
533
688
  if (userRoles && userRoles.size) {
534
- let grant = false;
535
- let deny = false;
689
+ let granted: string | undefined;
690
+ let denied: string | undefined;
536
691
  for (const r of userRoles) {
537
692
  const s = scope.byRole.get(r);
538
- if (s === 'denied') deny = true;
539
- else if (s === 'grant') grant = true;
693
+ if (s === 'denied') denied ??= r;
694
+ else if (s === 'grant') granted ??= r;
540
695
  }
541
- if (grant) return true;
542
- if (deny) return false;
696
+ if (granted !== undefined) return { allowed: true, scope, via: 'principal', key: granted };
697
+ if (denied !== undefined) return { allowed: false, scope, via: 'principal', key: denied };
543
698
  }
544
699
 
545
700
  // Tier 3 — the built-in `everyone` role (derived: a public plugin
546
701
  // principal granted here counts, see `AccessScope.everyone`).
547
- if (scope.everyone) return scope.everyone === 'grant';
702
+ if (scope.everyone) {
703
+ return { allowed: scope.everyone === 'grant', scope, via: 'everyone', key: EVERYONE_CANONICAL };
704
+ }
548
705
 
549
706
  // No verdict at this scope — fall through to the next (farther) one.
550
707
  }
551
708
 
552
- return false;
709
+ // No verdict anywhere (and no root access.md to stop at): the floor still holds.
710
+ return adminFloor
711
+ ? { allowed: true, scope: null, via: 'admin-floor' }
712
+ : { allowed: false, scope: null, via: 'default-deny' };
553
713
  }
554
714
 
555
715
  /**
@@ -581,6 +741,49 @@ function scopeToGrantSource(
581
741
  return { kind: 'ancestor', path };
582
742
  }
583
743
 
744
+ /**
745
+ * Where a scope's rules live, as the place a person edits: a folder (the
746
+ * directory of its `access.md`) or the target's own frontmatter.
747
+ */
748
+ function decisionSourceOf(
749
+ scope: AccessScope,
750
+ kind: AccessTargetKind,
751
+ relativePath: string,
752
+ ): AccessDecisionSource {
753
+ if (scope.source.kind === 'own') return { kind: 'frontmatter', path: relativePath, inherited: false };
754
+ const folder = path.posix.dirname(scope.source.path);
755
+ const dir = folder === '.' ? '' : folder;
756
+ const grant = scopeToGrantSource(scope, kind, relativePath);
757
+ return { kind: 'folder', path: dir, inherited: grant.kind === 'ancestor' };
758
+ }
759
+
760
+ /** A resolver decision in the terms `explainAccess` reports. */
761
+ function explainDecision(
762
+ model: AccessModel,
763
+ decision: PermissionDecision,
764
+ kind: AccessTargetKind,
765
+ relativePath: string,
766
+ ): AccessDecision {
767
+ const source = decision.scope ? decisionSourceOf(decision.scope, kind, relativePath) : null;
768
+ const base = { allowed: decision.allowed, source };
769
+ switch (decision.via) {
770
+ case 'admin-rescue':
771
+ case 'default-deny':
772
+ return { ...base, via: decision.via, principal: null };
773
+ case 'admin-floor':
774
+ return { ...base, via: decision.via, principal: 'Admin' };
775
+ case 'email':
776
+ return { ...base, via: 'person', principal: decision.key ?? null };
777
+ case 'everyone':
778
+ return { ...base, via: 'everyone', principal: EVERYONE_CANONICAL };
779
+ case 'principal': {
780
+ const key = decision.key ?? '';
781
+ const record = model.roles.byCanonical.get(key);
782
+ return { ...base, via: record?.kind ?? 'role', principal: record?.displayName ?? key };
783
+ }
784
+ }
785
+ }
786
+
584
787
  /**
585
788
  * Resolve EVERY file scope that names a principal for `verb`, ordered
586
789
  * closest-first — the source-returning twin of `hasPermissionResolved`, but
@@ -639,14 +842,7 @@ function resolveGrantSourcesForVerb(
639
842
  // a GROUP whose group has VANISHED reads "unshadowed" here, and the
640
843
  // alias-tolerant pair would then attribute a same-named role's
641
844
  // surviving `role/<name>` grant to the group.
642
- const canonical = canonicalRoleName(principal.role);
643
- const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
644
- const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
645
- const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
646
- const tokens =
647
- tokenMatch === 'exact' || shadowed
648
- ? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
649
- : [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
845
+ const tokens = ownTokensOf(model, principal.role, tokenMatch);
650
846
  for (const scope of scopes) {
651
847
  const states = tokens.map((t) => scope.byRole.get(t));
652
848
  if (states.includes('grant')) {
@@ -672,6 +868,84 @@ function resolveGrantSourcesForVerb(
672
868
  return out;
673
869
  }
674
870
 
871
+ /**
872
+ * WHICH token spellings in a file are THIS role-shaped principal's own entries
873
+ * — the shadowing rule, in one place because the grant walk, the denial walk
874
+ * and the revoke splice must all agree on it (a disagreement attributes a
875
+ * shadowed bare token to the wrong principal, which is how a "restrict" silently
876
+ * strips a same-named role's grant). See `resolveGrantSourcesForVerb` for the
877
+ * full reasoning behind each branch.
878
+ */
879
+ function ownTokensOf(
880
+ model: AccessModel,
881
+ role: string,
882
+ tokenMatch?: 'exact' | 'name',
883
+ ): string[] {
884
+ const canonical = canonicalRoleName(role);
885
+ const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
886
+ const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
887
+ const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
888
+ return tokenMatch === 'exact' || shadowed
889
+ ? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
890
+ : [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
891
+ }
892
+
893
+ /**
894
+ * The polarity twin of {@link resolveGrantSourcesForVerb}: every file scope
895
+ * whose own entry for this principal DENIES `verb`, closest-first.
896
+ *
897
+ * Same walk, mirrored stopping rule. A grant in the principal's own entry at a
898
+ * scope ENDS the list — under closest-wins a farther deny is dead, and reporting
899
+ * it would have the dialog say "restricted" about a verb the principal holds.
900
+ * A grant or deny reaching the principal through a group/role/`everyone` is not
901
+ * their own entry: it is neither a denial to report nor a reason to stop, so the
902
+ * walk carries on past it, exactly as the grant twin does.
903
+ *
904
+ * Returns `[]` when the principal is not denied `verb` by any file entry.
905
+ */
906
+ function resolveDenialSourcesForVerb(
907
+ model: AccessModel,
908
+ verb: Verb,
909
+ kind: AccessTargetKind,
910
+ relativePath: string,
911
+ principal: GrantPrincipal,
912
+ fileOwn?: OwnEntries | null,
913
+ tokenMatch?: 'exact' | 'name',
914
+ ): GrantSource[] {
915
+ const scopes = resolveScopes(model, verb, relativePath, fileOwn);
916
+ const out: GrantSource[] = [];
917
+
918
+ if (principal.kind === 'role') {
919
+ const tokens = ownTokensOf(model, principal.role, tokenMatch);
920
+ for (const scope of scopes) {
921
+ const states = tokens.map((t) => scope.byRole.get(t));
922
+ // A grant under EITHER spelling wins within the scope (`buildScope` lets a
923
+ // grant stick over a deny of the same principal), so it ends the walk
924
+ // whether or not the other spelling denies here.
925
+ if (states.includes('grant')) break;
926
+ // Only a `deny <verb>` LINE is a source; a denial the scope implies from
927
+ // a prerequisite verb's line (`deny write` when asking about owner) is
928
+ // real but is not this verb's entry — the prerequisite's own report is
929
+ // where it can be lifted.
930
+ const written = tokens.some(
931
+ (t) => scope.byRole.get(t) === 'denied' && !scope.implied.byRole.has(t),
932
+ );
933
+ if (written) out.push(scopeToGrantSource(scope, kind, relativePath));
934
+ }
935
+ return out;
936
+ }
937
+
938
+ const email = canonicalEmail(principal.email);
939
+ for (const scope of scopes) {
940
+ const own = scope.byEmail.get(email);
941
+ if (own === 'grant') break;
942
+ if (own === 'denied' && !scope.implied.byEmail.has(email)) {
943
+ out.push(scopeToGrantSource(scope, kind, relativePath));
944
+ }
945
+ }
946
+ return out;
947
+ }
948
+
675
949
  /**
676
950
  * Build the repo-root → path chain of directory scopes that govern
677
951
  * `relativePath`, e.g. `Knowledge/Sales/Foo.md` →
@@ -747,6 +1021,30 @@ function canReadResolved(
747
1021
  return hasPermissionResolved(model, 'read', userEmail, relativePath, fileOwn);
748
1022
  }
749
1023
 
1024
+ /**
1025
+ * The key set someone holding exactly the principal behind `token` carries —
1026
+ * the question "would a member of this principal be allowed?" put to
1027
+ * `hasPermissionForKeys`. A group gets its full member key set
1028
+ * (`principalKeysOfGroup`); a role or plugin principal gets every key its
1029
+ * record is registered under (the bare name and the `role/` alias share one
1030
+ * record) plus the plugin principals expanded from those keys; a token with no
1031
+ * record (`everyone`, a vanished principal) is just itself. Public keys are
1032
+ * always included — every caller holds them.
1033
+ */
1034
+ function principalKeysOfToken(model: AccessModel, token: string): Set<string> {
1035
+ const record = model.roles.byCanonical.get(token);
1036
+ if (record?.kind === 'group') return principalKeysOfGroup(model, token) ?? new Set([token]);
1037
+ const keys = new Set<string>([token]);
1038
+ if (!record) {
1039
+ for (const key of model.roles.publicKeys ?? []) keys.add(key);
1040
+ return keys;
1041
+ }
1042
+ for (const [key, principal] of model.roles.byCanonical) {
1043
+ if (principal === record) keys.add(key);
1044
+ }
1045
+ return addDerivedKeys(model, keys);
1046
+ }
1047
+
750
1048
  function eligibleHoldersResolved(
751
1049
  model: AccessModel,
752
1050
  verb: Verb,
@@ -771,16 +1069,31 @@ function eligibleHoldersResolved(
771
1069
  const key = `${kind}\0${name.toLowerCase()}`;
772
1070
  if (!byIdentity.has(key)) byIdentity.set(key, { name, kind });
773
1071
  };
1072
+ // Every row is re-checked against the resolver. The collapsed view keeps a
1073
+ // principal's closest OWN verdict, but a nearer scope can still decide for
1074
+ // them without naming them — a `deny everyone` in a subfolder cuts off a
1075
+ // root `write: Admin` — and a list that named them anyway would tell the
1076
+ // dialog and the denial message that someone is eligible whom the gate then
1077
+ // refuses.
1078
+ const allowedFor = (keys: ReadonlySet<string>) =>
1079
+ hasPermissionForKeys(model, verb, keys, relativePath, fileOwn);
774
1080
  for (const [canonical, state] of byRole) {
775
1081
  if (state !== 'grant') continue;
1082
+ if (!allowedFor(principalKeysOfToken(model, canonical))) continue;
776
1083
  const record = model.roles.byCanonical.get(canonical);
777
1084
  addPrincipal(record ? record.displayName : canonical, record?.kind ?? 'role');
778
1085
  }
1086
+ // The Admin write floor (see `hasPermissionResolved`): Admin holds write here
1087
+ // whenever no nearer scope excludes it, whether or not any file names it.
1088
+ if (verb === 'write' && model.roles.byCanonical.has(ADMIN_ROLE_KEY)) {
1089
+ const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY)!;
1090
+ if (allowedFor(principalKeysOfToken(model, ADMIN_ROLE_KEY))) addPrincipal(adminRole.displayName, 'role');
1091
+ }
779
1092
  // The derived everyone verdict: a public plugin principal granted here
780
1093
  // means anyone signed in holds the verb, and a list that counts holders (an
781
1094
  // approval gate, a "restricted to" banner) must say so — as `everyone`, the
782
1095
  // same row a literal grant produces.
783
- if (everyone === 'grant') addPrincipal(EVERYONE_CANONICAL, 'role');
1096
+ if (everyone === 'grant' && allowedFor(NO_KEYS)) addPrincipal(EVERYONE_CANONICAL, 'role');
784
1097
 
785
1098
  // Mirror the admin overrides applied in `hasPermissionResolved`: write on
786
1099
  // `roles.yaml` and on any `access.md` is granted to Admin even if the
@@ -794,7 +1107,7 @@ function eligibleHoldersResolved(
794
1107
  // Look the Admin ROLE up via its explicit alias — the bare key may be
795
1108
  // owned by a same-named group under group-first precedence. The override
796
1109
  // is the ROLE's capability, so the row's kind is 'role' regardless.
797
- const adminRole = model.roles.byCanonical.get(`${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`);
1110
+ const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY);
798
1111
  addPrincipal(adminRole ? adminRole.displayName : ADMIN_CANONICAL, 'role');
799
1112
  }
800
1113
 
@@ -809,6 +1122,7 @@ function eligibleHoldersResolved(
809
1122
  const users: { name: string; email: string }[] = [];
810
1123
  for (const [email, state] of byEmail) {
811
1124
  if (state !== 'grant') continue;
1125
+ if (!hasPermissionResolved(model, verb, email, relativePath, fileOwn)) continue;
812
1126
  users.push({ name: '', email });
813
1127
  }
814
1128
  users.sort((a, b) => a.email.localeCompare(b.email));
@@ -816,6 +1130,18 @@ function eligibleHoldersResolved(
816
1130
  return { principals, roles, users };
817
1131
  }
818
1132
 
1133
+ /**
1134
+ * Every individual the model names: `roles.yaml` members, emails named inline
1135
+ * at this path's scope, and the deployment owner (Admin for the write floor
1136
+ * even when `roles.yaml` does not list them).
1137
+ */
1138
+ function namedCandidateEmails(model: AccessModel, byEmail: CollapsedScope['byEmail']): Set<string> {
1139
+ const candidates = new Set<string>(model.deploymentOwners);
1140
+ for (const email of model.roles.byEmail.keys()) candidates.add(email);
1141
+ for (const email of byEmail.keys()) candidates.add(email);
1142
+ return candidates;
1143
+ }
1144
+
819
1145
  /**
820
1146
  * Expand the eligible-writer set to the underlying emails.
821
1147
  *
@@ -841,10 +1167,10 @@ function eligibleHolderEmailsResolved(
841
1167
  // in an access.md entry at this path's scope (whether granted or denied —
842
1168
  // `hasPermissionResolved` filters denials out below). The built-in
843
1169
  // `everyone` role can grant arbitrary signed-in users, but this finite
844
- // expansion can only return emails the access tree explicitly names.
845
- const candidates = new Set<string>();
846
- for (const email of model.roles.byEmail.keys()) candidates.add(email);
847
- for (const email of byEmail.keys()) candidates.add(email);
1170
+ // expansion can only return emails the access tree explicitly names. The
1171
+ // deployment owner is a candidate too: the Admin write floor admits them
1172
+ // whether or not `roles.yaml` lists them.
1173
+ const candidates = namedCandidateEmails(model, byEmail);
848
1174
 
849
1175
  for (const email of candidates) {
850
1176
  if (hasPermissionResolved(model, verb, email, relativePath, fileOwn)) {
@@ -871,9 +1197,7 @@ function ineligibleNamedEmailsResolved(
871
1197
  fileOwn?: OwnEntries | null,
872
1198
  ): Set<string> {
873
1199
  const { byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
874
- const candidates = new Set<string>();
875
- for (const email of model.roles.byEmail.keys()) candidates.add(email);
876
- for (const email of byEmail.keys()) candidates.add(email);
1200
+ const candidates = namedCandidateEmails(model, byEmail);
877
1201
 
878
1202
  const out = new Set<string>();
879
1203
  for (const email of candidates) {
@@ -962,6 +1286,7 @@ export class AccessControlService implements IAccessControl {
962
1286
  constructor(
963
1287
  private readonly workspaceService: WorkspaceService,
964
1288
  private readonly kbDirName: string,
1289
+ private readonly disk: ITreeWalker,
965
1290
  /**
966
1291
  * Emails that count as Admin for the two hardcoded `write` rescues,
967
1292
  * whatever `roles.yaml` says — in practice `ADMIN_EMAIL`. Optional so the
@@ -969,6 +1294,13 @@ export class AccessControlService implements IAccessControl {
969
1294
  * current behaviour (no owner, roles.yaml is the only authority).
970
1295
  */
971
1296
  deploymentOwners: readonly string[] = [],
1297
+ /**
1298
+ * How git is run — see `shared/git.contract.ts`. The composition root
1299
+ * passes the one runner; the default is the same runner on its default
1300
+ * deadline, kept for the same reason as `deploymentOwners` above: a great
1301
+ * many fixtures construct this service directly.
1302
+ */
1303
+ private readonly gitRunner: IGitRunner = new NodeGitRunner(),
972
1304
  ) {
973
1305
  this.deploymentOwners = new Set(
974
1306
  deploymentOwners.filter(Boolean).map((e) => canonicalEmail(e)),
@@ -1080,6 +1412,40 @@ export class AccessControlService implements IAccessControl {
1080
1412
  return result;
1081
1413
  }
1082
1414
 
1415
+ async canReadAsGroupBatch(
1416
+ workspaceId: string,
1417
+ group: string,
1418
+ relativePaths: string[],
1419
+ ): Promise<Map<string, boolean> | null> {
1420
+ const model = await this.loadModel(workspaceId);
1421
+ const keys = principalKeysOfGroup(model, group);
1422
+ if (keys === null) return null;
1423
+ const repoDir = await this.repoDir(workspaceId);
1424
+ // The same per-path own-entries `canReadBatch` folds in: a file's own
1425
+ // frontmatter rules are part of the walk for a group as for a person.
1426
+ const owns = await Promise.all(relativePaths.map((p) => this.cachedOwnEntries(workspaceId, repoDir, p)));
1427
+ const result = new Map<string, boolean>();
1428
+ relativePaths.forEach((p, i) => {
1429
+ result.set(p, hasPermissionForKeys(model, 'read', keys, p, owns[i]));
1430
+ });
1431
+ return result;
1432
+ }
1433
+
1434
+ async canReadAsEveryoneBatch(workspaceId: string, relativePaths: string[]): Promise<Map<string, boolean>> {
1435
+ const model = await this.loadModel(workspaceId);
1436
+ const repoDir = await this.repoDir(workspaceId);
1437
+ const owns = await Promise.all(relativePaths.map((p) => this.cachedOwnEntries(workspaceId, repoDir, p)));
1438
+ const result = new Map<string, boolean>();
1439
+ relativePaths.forEach((p, i) => {
1440
+ // No keys at all: the walk then falls through to each scope's derived
1441
+ // `everyone` verdict — the built-in entry, or a public plugin's grant
1442
+ // or denial, whichever the scope builder settled on — which is exactly
1443
+ // what a caller holding nothing of their own resolves to.
1444
+ result.set(p, hasPermissionForKeys(model, 'read', NO_KEYS, p, owns[i]));
1445
+ });
1446
+ return result;
1447
+ }
1448
+
1083
1449
  async canWriteBatch(
1084
1450
  workspaceId: string,
1085
1451
  userEmail: string,
@@ -1206,6 +1572,52 @@ export class AccessControlService implements IAccessControl {
1206
1572
  return eligibleHoldersResolved(model, 'download', relativePath, own);
1207
1573
  }
1208
1574
 
1575
+ /**
1576
+ * The read and write holders of one file at its current path and at a path
1577
+ * it has not moved to yet (see the interface).
1578
+ *
1579
+ * A move changes nothing about the file's own frontmatter — that travels
1580
+ * with the bytes — and everything about the folder chain above it. So the
1581
+ * hypothetical side is the ordinary resolution with the destination path
1582
+ * substituted: the same model, the same own-entries (read from the SOURCE,
1583
+ * since nothing sits at the destination to read), resolved against the
1584
+ * destination's ancestors. One model load and one file read serve all four
1585
+ * lookups.
1586
+ */
1587
+ async prospectiveHolders(
1588
+ workspaceId: string,
1589
+ fromPath: string,
1590
+ toPath: string,
1591
+ ): Promise<ProspectiveHolders> {
1592
+ const model = await this.loadModel(workspaceId);
1593
+ const repoDir = await this.repoDir(workspaceId);
1594
+ // A FILE question only. A folder carries its own `access.md` — which moves
1595
+ // with it and governs everything under it — so resolving it as a file
1596
+ // would read frontmatter it does not have and omit the rules it does,
1597
+ // naming principals that are not the ones a folder move changes. Refuse
1598
+ // rather than answer the wrong question convincingly.
1599
+ //
1600
+ // Only absence is an answer here (see `isAbsence`): a probe that failed on
1601
+ // permissions or I/O does not say "this is a file", and folding it into
1602
+ // one would let the very case above through on an unreadable source.
1603
+ const fromStat = await fs.stat(path.join(repoDir, fromPath)).catch((err: unknown) => {
1604
+ if (isAbsence(err)) return null;
1605
+ throw err;
1606
+ });
1607
+ if (fromStat?.isDirectory()) {
1608
+ throw new WorkflowDomainError(
1609
+ 'prospective access answers for a file, not a folder',
1610
+ 400,
1611
+ );
1612
+ }
1613
+ const own = await this.readOwnEntries(repoDir, fromPath);
1614
+ const holdersAt = (relativePath: string): PathHolders => ({
1615
+ read: eligibleHoldersResolved(model, 'read', relativePath, own),
1616
+ write: eligibleHoldersResolved(model, 'write', relativePath, own),
1617
+ });
1618
+ return { before: holdersAt(fromPath), after: holdersAt(toPath) };
1619
+ }
1620
+
1209
1621
  async eligibleWriterEmails(
1210
1622
  workspaceId: string,
1211
1623
  relativePath: string,
@@ -1255,6 +1667,111 @@ export class AccessControlService implements IAccessControl {
1255
1667
  return out;
1256
1668
  }
1257
1669
 
1670
+ async denialSources(
1671
+ workspaceId: string,
1672
+ kind: AccessTargetKind,
1673
+ relativePath: string,
1674
+ principal: GrantPrincipal,
1675
+ opts?: { tokenMatch?: 'exact' | 'name' },
1676
+ ): Promise<DenialSources> {
1677
+ const model = await this.loadModel(workspaceId);
1678
+ // Same own-scope rule as `grantSources`: a file target consults its own
1679
+ // frontmatter, a folder target's own access.md is already in the dir chain.
1680
+ const own =
1681
+ kind === 'file'
1682
+ ? await this.readOwnEntries(await this.repoDir(workspaceId), relativePath)
1683
+ : null;
1684
+ const out: DenialSources = {};
1685
+ for (const verb of KNOWN_VERBS) {
1686
+ const sources = resolveDenialSourcesForVerb(
1687
+ model,
1688
+ verb,
1689
+ kind,
1690
+ relativePath,
1691
+ principal,
1692
+ own,
1693
+ opts?.tokenMatch,
1694
+ );
1695
+ if (sources.length > 0) out[verb] = sources;
1696
+ }
1697
+ return out;
1698
+ }
1699
+
1700
+ async locallyDeniedPrincipals(
1701
+ workspaceId: string,
1702
+ kind: AccessTargetKind,
1703
+ relativePath: string,
1704
+ ): Promise<{ principals: ResolvedPrincipal[]; users: { name: string; email: string }[] }> {
1705
+ const model = await this.loadModel(workspaceId);
1706
+ // The target's OWN entries, and only those — the one scope whose rules a
1707
+ // restriction made here is written into. A folder's are its `access.md` in
1708
+ // the dir model; a file node's are its own frontmatter.
1709
+ const own: OwnEntries | null =
1710
+ kind === 'folder'
1711
+ ? (model.accessFilesByDir.get(relativePath)?.entries ?? null)
1712
+ : await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
1713
+ if (!own) return { principals: [], users: [] };
1714
+
1715
+ const byIdentity = new Map<string, ResolvedPrincipal>();
1716
+ const byEmail = new Map<string, { name: string; email: string }>();
1717
+ for (const verb of KNOWN_VERBS) {
1718
+ for (const entry of own[verb] ?? []) {
1719
+ // Grants are already reported by the eligible lists; only the denials
1720
+ // are invisible there, and only they are this method's business.
1721
+ if (!entry.deny) continue;
1722
+ if (entry.kind === 'user') {
1723
+ if (!byEmail.has(entry.email)) {
1724
+ byEmail.set(entry.email, { name: entry.displayName, email: entry.email });
1725
+ }
1726
+ continue;
1727
+ }
1728
+ // A token naming a role nobody knows is not a rule: `loadModel` drops
1729
+ // such entries from every `access.md`, and `resolveScopes` filters them
1730
+ // out of a file's own frontmatter (the one scope it does not pre-filter)
1731
+ // — so the resolver denies nothing, `denialSources` reports nothing, and
1732
+ // a row here would claim a restriction the server does not enforce. Skip
1733
+ // it on the same rule, so this list and the denial sources agree.
1734
+ if (!roleKnown(model.roles, entry.role)) continue;
1735
+ // Resolve the token's kind and display name through the SAME merged
1736
+ // index the eligible lists use, so a denied group keys as `g:` and a
1737
+ // denied role as `r:` — matching the row keys the view is built on. A
1738
+ // KNOWN token with no record is a built-in (`everyone`, `admin` before
1739
+ // roles.yaml names it), which displays as the role it is.
1740
+ const record = model.roles.byCanonical.get(entry.role);
1741
+ const name = record ? record.displayName : (entry.displayRole || entry.role);
1742
+ const kindOf = record?.kind ?? 'role';
1743
+ const key = `${kindOf}\0${name.toLowerCase()}`;
1744
+ if (!byIdentity.has(key)) byIdentity.set(key, { name, kind: kindOf });
1745
+ }
1746
+ }
1747
+ const principals = [...byIdentity.values()].sort((a, b) =>
1748
+ a.name < b.name ? -1 : a.name > b.name ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0,
1749
+ );
1750
+ const users = [...byEmail.values()].sort((a, b) => a.email.localeCompare(b.email));
1751
+ return { principals, users };
1752
+ }
1753
+
1754
+ async explainAccess(
1755
+ workspaceId: string,
1756
+ userEmail: string,
1757
+ kind: AccessTargetKind,
1758
+ relativePath: string,
1759
+ ): Promise<Record<Verb, AccessDecision>> {
1760
+ const model = await this.loadModel(workspaceId);
1761
+ // The same own-entries read `canRead`/`canWrite`/… make, whatever the
1762
+ // kind: a directory simply has none.
1763
+ const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
1764
+ const out = {} as Record<Verb, AccessDecision>;
1765
+ for (const verb of KNOWN_VERBS) {
1766
+ const machineOwned = verb === 'write' ? this.machineOwnedWriteRule(userEmail, relativePath) : null;
1767
+ out[verb] =
1768
+ machineOwned !== null
1769
+ ? { allowed: machineOwned, source: null, via: 'machine-owned', principal: null }
1770
+ : explainDecision(model, decidePermissionResolved(model, verb, userEmail, relativePath, own), kind, relativePath);
1771
+ }
1772
+ return out;
1773
+ }
1774
+
1258
1775
  async kbPrincipals(workspaceId: string): Promise<{
1259
1776
  roles: string[];
1260
1777
  groups: string[];
@@ -1355,7 +1872,7 @@ export class AccessControlService implements IAccessControl {
1355
1872
  ...namesByEmail.keys(),
1356
1873
  ]);
1357
1874
  for (const email of candidates) {
1358
- if (sha256Email(email) === hash) {
1875
+ if (hashEmail(email) === hash) {
1359
1876
  const displayName = namesByEmail.get(email) ?? email.split('@')[0];
1360
1877
  return { email, displayName };
1361
1878
  }
@@ -1374,7 +1891,7 @@ export class AccessControlService implements IAccessControl {
1374
1891
  */
1375
1892
  private machineOwnedWriteRule(userEmail: string, relativePath: string): boolean | null {
1376
1893
  if (relativePath !== SYNCED_GROUPS_YAML) return null;
1377
- return userEmail.trim().toLowerCase() === DIRECTORY_SYNC_BOT_EMAIL;
1894
+ return canonicalEmail(userEmail) === DIRECTORY_SYNC_BOT_EMAIL;
1378
1895
  }
1379
1896
 
1380
1897
  async canWriteAtRef(
@@ -1455,6 +1972,54 @@ export class AccessControlService implements IAccessControl {
1455
1972
  return eligibleHoldersResolved(loaded.model, 'write', relativePath, own);
1456
1973
  }
1457
1974
 
1975
+ async holdsAdminRootWrite(workspaceId: string, userEmail: string): Promise<boolean> {
1976
+ const model = await this.loadModel(workspaceId);
1977
+ return isAdminEmail(model, canonicalEmail(userEmail));
1978
+ }
1979
+
1980
+ async canRestorePlatformFile(
1981
+ workspaceId: string,
1982
+ userEmail: string,
1983
+ destinationRelativePath: string,
1984
+ ): Promise<boolean> {
1985
+ const target = platformRestoreDestination(destinationRelativePath);
1986
+ if (target === null) return false;
1987
+ const email = canonicalEmail(userEmail);
1988
+
1989
+ let admin: boolean;
1990
+ try {
1991
+ admin = isAdminEmail(await this.loadModel(workspaceId), email);
1992
+ } catch {
1993
+ // The repository this rescue exists for is exactly the one whose
1994
+ // `roles.yaml` may be the file that went missing, and `loadModel`
1995
+ // throws when it cannot be read. With no model to ask, the deployment
1996
+ // owner is the only admin left — and they are the person who sets
1997
+ // `ADMIN_EMAIL`, so admitting them concedes nothing they did not have.
1998
+ admin = this.deploymentOwners.has(email);
1999
+ }
2000
+ if (!admin) return false;
2001
+
2002
+ // A restore puts back what is MISSING, so the place it lands must be
2003
+ // empty. An `access.md` goes back only into a folder that has none —
2004
+ // into a folder that already has one it would not be a restore but a rule
2005
+ // change wearing a move's clothes, and the destination's own rules would
2006
+ // be the thing it bypassed the write gate to overwrite. The same holds at
2007
+ // the root: a move is a rename on disk, so landing `.bevelignore` on a
2008
+ // root that already has one would silently replace it, and the repository
2009
+ // was never missing that file to begin with.
2010
+ const destination =
2011
+ target.kind === 'root' ? target.name : path.join(target.dir, 'access.md');
2012
+ const repoDir = await this.repoDir(workspaceId);
2013
+ try {
2014
+ await fs.stat(path.join(repoDir, destination));
2015
+ return false;
2016
+ } catch (err) {
2017
+ if (isAbsence(err)) return true;
2018
+ // Anything other than genuine absence is not an answer: fail closed.
2019
+ return false;
2020
+ }
2021
+ }
2022
+
1458
2023
  async eligibleWritersForPathsAtRef(
1459
2024
  workspaceId: string,
1460
2025
  ref: string,
@@ -1545,23 +2110,22 @@ export class AccessControlService implements IAccessControl {
1545
2110
  try {
1546
2111
  return await fs.readFile(path.join(repoDir, filename), 'utf-8');
1547
2112
  } catch (err) {
1548
- const code = (err as NodeJS.ErrnoException | null)?.code;
1549
- if (code === 'ENOENT' || code === 'ENOTDIR') return null;
2113
+ if (isAbsence(err)) return null;
1550
2114
  throw err;
1551
2115
  }
1552
2116
  });
1553
2117
  if (!activeGroups.health.ok) {
1554
- console.error(
1555
- `[access] groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`,
2118
+ log.error(
2119
+ `groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`,
1556
2120
  );
1557
2121
  }
1558
- for (const w of activeGroups.warnings) console.warn(`[access] ${w}`);
2122
+ for (const w of activeGroups.warnings) log.warn(w);
1559
2123
  const mergeWarnings = mergeGroupsIntoRoles(
1560
2124
  rolesParsed.index,
1561
2125
  activeGroups.groups,
1562
2126
  activeGroups.sourceFile,
1563
2127
  );
1564
- for (const w of mergeWarnings) console.warn(`[access] ${w}`);
2128
+ for (const w of mergeWarnings) log.warn(w);
1565
2129
 
1566
2130
  const accessFiles = new Map<string, AccessFile>();
1567
2131
 
@@ -1579,7 +2143,7 @@ export class AccessControlService implements IAccessControl {
1579
2143
  // / `roles.yaml` because `hasPermissionResolved` admin-rescues those
1580
2144
  // paths, so the bad config remains fixable from inside the app.
1581
2145
  const pluginDirs = new Map<string, string>();
1582
- await walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
2146
+ await this.disk.walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
1583
2147
 
1584
2148
  // Plugin principals (`plugin/<Name>/<verb>`), derived from each plugin
1585
2149
  // folder's own access.md — a plugin being a folder with a manifest, at
@@ -1596,9 +2160,7 @@ export class AccessControlService implements IAccessControl {
1596
2160
  for (const verb of KNOWN_VERBS) {
1597
2161
  file.entries[verb] = file.entries[verb].filter((entry) => {
1598
2162
  if (entry.kind === 'role' && !roleKnown(rolesParsed.index, entry.role)) {
1599
- console.warn(
1600
- `[access] ${file.path}: '${verb}' references unknown role '${entry.displayRole}' — entry ignored`,
1601
- );
2163
+ log.warn(`${file.path}: '${verb}' references unknown role '${entry.displayRole}' — entry ignored`);
1602
2164
  return false;
1603
2165
  }
1604
2166
  return true;
@@ -1686,7 +2248,7 @@ export class AccessControlService implements IAccessControl {
1686
2248
  else if (!wanted.includes(p)) wanted.push(p);
1687
2249
  }
1688
2250
  if (wanted.length === 0) return result;
1689
- const texts = await catFileBatch(repoDir, wanted.map((p) => `${ref}:${p}`));
2251
+ const texts = await catFileBatch(this.gitRunner, repoDir, wanted.map((p) => `${ref}:${p}`));
1690
2252
  wanted.forEach((p, i) => {
1691
2253
  const text = texts[i];
1692
2254
  if (text === null) result.set(p, null);
@@ -1730,15 +2292,7 @@ export class AccessControlService implements IAccessControl {
1730
2292
  relativePath: string,
1731
2293
  ): Promise<{ kind: 'text'; text: string } | { kind: 'absent' } | { kind: 'error' }> {
1732
2294
  try {
1733
- const { stdout } = await execFileAsync(
1734
- 'git',
1735
- ['-C', repoDir, 'show', `${ref}:${relativePath}`],
1736
- {
1737
- encoding: 'utf-8',
1738
- maxBuffer: 16 * 1024 * 1024,
1739
- env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
1740
- },
1741
- );
2295
+ const { stdout } = await this.gitRunner.run(repoDir, ['show', `${ref}:${relativePath}`]);
1742
2296
  return { kind: 'text', text: stdout };
1743
2297
  } catch (err) {
1744
2298
  const stderr =
@@ -1763,11 +2317,7 @@ export class AccessControlService implements IAccessControl {
1763
2317
  ref: string,
1764
2318
  ): Promise<{ accessFiles: string[]; pluginDirs: string[] } | 'error'> {
1765
2319
  try {
1766
- const { stdout } = await execFileAsync(
1767
- 'git',
1768
- ['-C', repoDir, 'ls-tree', '-r', '--name-only', ref],
1769
- { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 },
1770
- );
2320
+ const { stdout } = await this.gitRunner.run(repoDir, ['ls-tree', '-r', '--name-only', ref]);
1771
2321
  const out: string[] = [];
1772
2322
  const pluginDirs: string[] = [];
1773
2323
  for (const line of stdout.split('\n')) {
@@ -1850,14 +2400,19 @@ export class AccessControlService implements IAccessControl {
1850
2400
  private async revParseCommit(repoDir: string, ref: string): Promise<string | null> {
1851
2401
  if (!ref || ref.startsWith('-')) return null;
1852
2402
  try {
1853
- const { stdout } = await execFileAsync(
1854
- 'git',
1855
- ['-C', repoDir, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`],
1856
- { encoding: 'utf-8' },
1857
- );
2403
+ const { stdout } = await this.gitRunner.run(repoDir, [
2404
+ 'rev-parse',
2405
+ '--verify',
2406
+ '--quiet',
2407
+ `${ref}^{commit}`,
2408
+ ]);
1858
2409
  const sha = stdout.trim();
1859
2410
  return /^[0-9a-f]{40,64}$/.test(sha) ? sha : null;
1860
- } catch {
2411
+ } catch (err) {
2412
+ // Null means "does not resolve". A deadline says nothing about the ref,
2413
+ // and as a null it would turn a stalled git into a gate that quietly
2414
+ // reads no roles at that ref; it surfaces instead, as a read failure.
2415
+ if (isGitTimeout(err)) throw err;
1861
2416
  return null;
1862
2417
  }
1863
2418
  }
@@ -1894,7 +2449,7 @@ export class AccessControlService implements IAccessControl {
1894
2449
  if (outcome.kind === 'error') {
1895
2450
  // The operational signal: one line per failed read, naming the commit
1896
2451
  // and the path, so a run of these is visible in the logs.
1897
- console.warn(`[access@${tag}] git read of ${relativePath} failed; refusing to decide from a partial tree`);
2452
+ logger(`access@${tag}`).warn(`git read of ${relativePath} failed; refusing to decide from a partial tree`);
1898
2453
  throw new AccessUnreadableError(label, relativePath);
1899
2454
  }
1900
2455
  return outcome.kind === 'text' ? outcome.text : null;
@@ -1914,22 +2469,22 @@ export class AccessControlService implements IAccessControl {
1914
2469
  // fires only on a file that was read and would not parse.
1915
2470
  const activeGroups = await loadActiveGroups(read);
1916
2471
  if (!activeGroups.health.ok) {
1917
- console.error(
1918
- `[access@${tag}] groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`,
2472
+ logger(`access@${tag}`).error(
2473
+ `groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`,
1919
2474
  );
1920
2475
  }
1921
- for (const w of activeGroups.warnings) console.warn(`[access@${tag}] ${w}`);
2476
+ for (const w of activeGroups.warnings) logger(`access@${tag}`).warn(w);
1922
2477
  const mergeWarnings = mergeGroupsIntoRoles(
1923
2478
  rolesParsed.index,
1924
2479
  activeGroups.groups,
1925
2480
  activeGroups.sourceFile,
1926
2481
  );
1927
- for (const w of mergeWarnings) console.warn(`[access@${tag}] ${w}`);
2482
+ for (const w of mergeWarnings) logger(`access@${tag}`).warn(w);
1928
2483
 
1929
2484
  const accessFiles = new Map<string, AccessFile>();
1930
2485
  const listed = await this.listAccessFilesAtRef(repoDir, commit);
1931
2486
  if (listed === 'error') {
1932
- console.warn(`[access@${tag}] listing access.md files failed; refusing to decide from a partial tree`);
2487
+ logger(`access@${tag}`).warn('listing access.md files failed; refusing to decide from a partial tree');
1933
2488
  throw new AccessUnreadableError(label, 'access.md (ls-tree)');
1934
2489
  }
1935
2490
  for (const p of listed.accessFiles) {