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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1307) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -8
  2. package/dist/core/catalog-cache-invalidation.d.ts +45 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +76 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/catalog-revision.d.ts +39 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -0
  8. package/dist/core/catalog-revision.js +94 -0
  9. package/dist/core/catalog-revision.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +7 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +279 -64
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +66 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +290 -111
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +138 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -0
  20. package/dist/core/lifecycle.js +218 -0
  21. package/dist/core/lifecycle.js.map +1 -0
  22. package/dist/core/public-config.d.ts +82 -0
  23. package/dist/core/public-config.d.ts.map +1 -0
  24. package/dist/core/public-config.js +78 -0
  25. package/dist/core/public-config.js.map +1 -0
  26. package/dist/core/readiness.d.ts +86 -0
  27. package/dist/core/readiness.d.ts.map +1 -0
  28. package/dist/core/readiness.js +56 -0
  29. package/dist/core/readiness.js.map +1 -0
  30. package/dist/core-config.d.ts +30 -0
  31. package/dist/core-config.d.ts.map +1 -1
  32. package/dist/core-config.js +64 -2
  33. package/dist/core-config.js.map +1 -1
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/access/access-control.interface.d.ts +231 -15
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +83 -12
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +815 -212
  43. package/dist/modules/access/access-control.service.js.map +1 -1
  44. package/dist/modules/access/access-mutation.service.d.ts +71 -5
  45. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-mutation.service.js +161 -13
  47. package/dist/modules/access/access-mutation.service.js.map +1 -1
  48. package/dist/modules/access/access-view.d.ts +147 -0
  49. package/dist/modules/access/access-view.d.ts.map +1 -0
  50. package/dist/modules/access/access-view.js +211 -0
  51. package/dist/modules/access/access-view.js.map +1 -0
  52. package/dist/modules/access/access.routes.d.ts.map +1 -1
  53. package/dist/modules/access/access.routes.js +312 -84
  54. package/dist/modules/access/access.routes.js.map +1 -1
  55. package/dist/modules/access/admin-locked-commit.d.ts +2 -3
  56. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  57. package/dist/modules/access/admin-locked-commit.js +9 -8
  58. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  59. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
  60. package/dist/modules/access/admin-route-helpers.js +4 -4
  61. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  62. package/dist/modules/access/change-read-gate.d.ts +53 -0
  63. package/dist/modules/access/change-read-gate.d.ts.map +1 -0
  64. package/dist/modules/access/change-read-gate.js +134 -0
  65. package/dist/modules/access/change-read-gate.js.map +1 -0
  66. package/dist/modules/access/creator-access.d.ts +33 -39
  67. package/dist/modules/access/creator-access.d.ts.map +1 -1
  68. package/dist/modules/access/creator-access.js +92 -119
  69. package/dist/modules/access/creator-access.js.map +1 -1
  70. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  71. package/dist/modules/access/directory-sync-bot.js +3 -1
  72. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  73. package/dist/modules/access/groups-admin.service.d.ts +3 -3
  74. package/dist/modules/access/groups-admin.service.js +2 -2
  75. package/dist/modules/access/reference-scan.d.ts +1 -1
  76. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  77. package/dist/modules/access/reference-scan.js +5 -4
  78. package/dist/modules/access/reference-scan.js.map +1 -1
  79. package/dist/modules/access/roles-admin.service.d.ts +72 -13
  80. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  81. package/dist/modules/access/roles-admin.service.js +71 -13
  82. package/dist/modules/access/roles-admin.service.js.map +1 -1
  83. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  84. package/dist/modules/access/synced-groups-committer.js +8 -6
  85. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
  87. package/dist/modules/access/synced-groups-writer.js +3 -8
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  89. package/dist/modules/access/user-access-removal.service.d.ts +209 -0
  90. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
  91. package/dist/modules/access/user-access-removal.service.js +662 -0
  92. package/dist/modules/access/user-access-removal.service.js.map +1 -0
  93. package/dist/modules/access-model/access-errors.d.ts +32 -1
  94. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  95. package/dist/modules/access-model/access-errors.js +38 -11
  96. package/dist/modules/access-model/access-errors.js.map +1 -1
  97. package/dist/modules/access-model/access-grammar.d.ts +100 -25
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +162 -48
  100. package/dist/modules/access-model/access-grammar.js.map +1 -1
  101. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  102. package/dist/modules/access-model/access-splice.js +18 -18
  103. package/dist/modules/access-model/access-splice.js.map +1 -1
  104. package/dist/modules/access-model/access-template.d.ts +22 -0
  105. package/dist/modules/access-model/access-template.d.ts.map +1 -0
  106. package/dist/modules/access-model/access-template.js +42 -0
  107. package/dist/modules/access-model/access-template.js.map +1 -0
  108. package/dist/modules/access-model/change-gate.d.ts +65 -0
  109. package/dist/modules/access-model/change-gate.d.ts.map +1 -0
  110. package/dist/modules/access-model/change-gate.js +25 -0
  111. package/dist/modules/access-model/change-gate.js.map +1 -0
  112. package/dist/modules/access-model/creator.d.ts +13 -16
  113. package/dist/modules/access-model/creator.d.ts.map +1 -1
  114. package/dist/modules/access-model/creator.js.map +1 -1
  115. package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
  116. package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
  117. package/dist/modules/access-model/frontmatter-lines.js +74 -0
  118. package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
  119. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  120. package/dist/modules/access-model/group-files.js +4 -1
  121. package/dist/modules/access-model/group-files.js.map +1 -1
  122. package/dist/modules/access-model/plugin-principals.d.ts +51 -0
  123. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  124. package/dist/modules/access-model/plugin-principals.js +119 -0
  125. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  126. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  127. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  128. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  129. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  130. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  131. package/dist/modules/admin/admin-access.service.js +5 -2
  132. package/dist/modules/admin/admin-access.service.js.map +1 -1
  133. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  134. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  135. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  136. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  137. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  138. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  139. package/dist/modules/agent-instructions/compose.js +234 -0
  140. package/dist/modules/agent-instructions/compose.js.map +1 -0
  141. package/dist/modules/agent-instructions/index.d.ts +4 -0
  142. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  143. package/dist/modules/agent-instructions/index.js +4 -0
  144. package/dist/modules/agent-instructions/index.js.map +1 -0
  145. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  146. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  147. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  148. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  149. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  150. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  151. package/dist/modules/auth/account-erasure.service.js +96 -10
  152. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  153. package/dist/modules/auth/account.routes.d.ts +6 -3
  154. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  155. package/dist/modules/auth/account.routes.js +109 -10
  156. package/dist/modules/auth/account.routes.js.map +1 -1
  157. package/dist/modules/auth/auth.routes.d.ts +10 -3
  158. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  159. package/dist/modules/auth/auth.routes.js +9 -4
  160. package/dist/modules/auth/auth.routes.js.map +1 -1
  161. package/dist/modules/auth/auth.service.d.ts +89 -5
  162. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  163. package/dist/modules/auth/auth.service.js +160 -25
  164. package/dist/modules/auth/auth.service.js.map +1 -1
  165. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  166. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  167. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  168. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  169. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  170. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  171. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  172. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  173. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  174. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  175. package/dist/modules/database/advisory-lock.d.ts +103 -0
  176. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  177. package/dist/modules/database/advisory-lock.js +320 -0
  178. package/dist/modules/database/advisory-lock.js.map +1 -0
  179. package/dist/modules/database/connection.d.ts.map +1 -1
  180. package/dist/modules/database/connection.js +9 -1
  181. package/dist/modules/database/connection.js.map +1 -1
  182. package/dist/modules/database/core-schema.d.ts +672 -0
  183. package/dist/modules/database/core-schema.d.ts.map +1 -1
  184. package/dist/modules/database/core-schema.js +171 -0
  185. package/dist/modules/database/core-schema.js.map +1 -1
  186. package/dist/modules/database/migrate.d.ts.map +1 -1
  187. package/dist/modules/database/migrate.js +26 -6
  188. package/dist/modules/database/migrate.js.map +1 -1
  189. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  190. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  191. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  192. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  193. package/dist/modules/diff/diff.routes.js +2 -4
  194. package/dist/modules/diff/diff.routes.js.map +1 -1
  195. package/dist/modules/diff/diff.service.d.ts +4 -1
  196. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  197. package/dist/modules/diff/diff.service.js +79 -81
  198. package/dist/modules/diff/diff.service.js.map +1 -1
  199. package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
  200. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  201. package/dist/modules/kb-fs/bevel-ignore.js +71 -0
  202. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  203. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  204. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  205. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  206. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
  207. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  208. package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
  209. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  210. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  211. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  212. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  213. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  214. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  215. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  216. package/dist/modules/kb-fs/node-fs.js +158 -0
  217. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  218. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  219. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  220. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  221. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  222. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  223. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  224. package/dist/modules/kb-fs/repo-path.js +195 -3
  225. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  226. package/dist/modules/kb-sync/index.d.ts +6 -0
  227. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  228. package/dist/modules/kb-sync/index.js +5 -0
  229. package/dist/modules/kb-sync/index.js.map +1 -0
  230. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  231. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  232. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  233. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  234. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  235. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  236. package/dist/modules/kb-sync/kb-sync.routes.js +137 -0
  237. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  238. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  239. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  240. package/dist/modules/kb-sync/kb-sync.service.js +185 -0
  241. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  242. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  243. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  244. package/dist/modules/kb-sync/sync-auth.js +62 -0
  245. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  246. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  247. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  248. package/dist/modules/kb-sync/sync-payload.js +105 -0
  249. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  250. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  251. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  252. package/dist/modules/marketplace/git-http.routes.js +192 -0
  253. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  254. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +33 -0
  255. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  256. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +88 -0
  257. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  258. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  259. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  260. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  261. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  262. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +126 -0
  263. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  264. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +192 -0
  265. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  266. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  267. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  268. package/dist/modules/marketplace/github-facade/github-facade.routes.js +252 -0
  269. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  270. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +121 -0
  271. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  272. package/dist/modules/marketplace/github-facade/github-facade.service.js +213 -0
  273. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  274. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  275. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  276. package/dist/modules/marketplace/github-facade/index.js +6 -0
  277. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  278. package/dist/modules/marketplace/index.d.ts +4 -0
  279. package/dist/modules/marketplace/index.d.ts.map +1 -0
  280. package/dist/modules/marketplace/index.js +4 -0
  281. package/dist/modules/marketplace/index.js.map +1 -0
  282. package/dist/modules/marketplace/marketplace-repo.service.d.ts +135 -0
  283. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  284. package/dist/modules/marketplace/marketplace-repo.service.js +318 -0
  285. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  286. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  287. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  288. package/dist/modules/mcp/downstream-pool.js +221 -0
  289. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  290. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  291. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  292. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  293. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  294. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  295. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  296. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  297. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  298. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  299. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  300. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  301. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  302. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  303. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  304. package/dist/modules/mcp/first-call-probe.js +298 -0
  305. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  306. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  307. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  308. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  309. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  310. package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
  311. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  312. package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
  313. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  314. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  315. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  316. package/dist/modules/mcp/mcp.routes.js +83 -179
  317. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  318. package/dist/modules/mcp/mcp.service.d.ts +218 -61
  319. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  320. package/dist/modules/mcp/mcp.service.js +713 -159
  321. package/dist/modules/mcp/mcp.service.js.map +1 -1
  322. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  323. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +8 -2
  324. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  325. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  326. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  327. package/dist/modules/mcp/oauth/oauth-consent.routes.js +16 -2
  328. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  329. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  330. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  331. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  332. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  333. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  334. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  335. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  336. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  337. package/dist/modules/plugins/compile/compile-marketplace.d.ts +104 -0
  338. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  339. package/dist/modules/plugins/compile/compile-marketplace.js +323 -0
  340. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  341. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +73 -0
  342. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  343. package/dist/modules/plugins/compile/marketplace-compiler.service.js +135 -0
  344. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  345. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  346. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  347. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +158 -0
  348. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  349. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +65 -0
  350. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  351. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +204 -0
  352. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  353. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +39 -0
  354. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  355. package/dist/modules/plugins/discovery/kb-plugin-source.js +110 -0
  356. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  357. package/dist/modules/plugins/discovery/native.source.d.ts +16 -0
  358. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  359. package/dist/modules/plugins/discovery/native.source.js +120 -0
  360. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  361. package/dist/modules/plugins/discovery/plugin-source.d.ts +110 -0
  362. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  363. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  364. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  365. package/dist/modules/plugins/index.d.ts +15 -3
  366. package/dist/modules/plugins/index.d.ts.map +1 -1
  367. package/dist/modules/plugins/index.js +13 -2
  368. package/dist/modules/plugins/index.js.map +1 -1
  369. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  370. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  371. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  372. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  373. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  374. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  375. package/dist/modules/plugins/join-request-records.store.js +179 -0
  376. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  377. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  378. package/dist/modules/plugins/join-requests.service.js +4 -2
  379. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  380. package/dist/modules/plugins/plugin-links.d.ts +69 -0
  381. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  382. package/dist/modules/plugins/plugin-links.js +163 -0
  383. package/dist/modules/plugins/plugin-links.js.map +1 -0
  384. package/dist/modules/plugins/plugin-links.service.d.ts +111 -0
  385. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  386. package/dist/modules/plugins/plugin-links.service.js +295 -0
  387. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  388. package/dist/modules/plugins/plugin-provision.service.d.ts +129 -21
  389. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  390. package/dist/modules/plugins/plugin-provision.service.js +320 -75
  391. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  392. package/dist/modules/plugins/plugin-rename.service.d.ts +71 -0
  393. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  394. package/dist/modules/plugins/plugin-rename.service.js +254 -0
  395. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  396. package/dist/modules/plugins/plugins.contract.d.ts +78 -4
  397. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  398. package/dist/modules/plugins/plugins.routes.d.ts +44 -5
  399. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  400. package/dist/modules/plugins/plugins.routes.js +297 -115
  401. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  402. package/dist/modules/plugins/plugins.service.d.ts +82 -3
  403. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  404. package/dist/modules/plugins/plugins.service.js +178 -48
  405. package/dist/modules/plugins/plugins.service.js.map +1 -1
  406. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  407. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  408. package/dist/modules/plugins/plugins.tools.js +82 -0
  409. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  410. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  411. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  412. package/dist/modules/plugins/teams.routes.js +79 -0
  413. package/dist/modules/plugins/teams.routes.js.map +1 -0
  414. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  415. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  416. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  417. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  418. package/dist/modules/secrets-vault/index.d.ts +1 -1
  419. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  420. package/dist/modules/secrets-vault/index.js.map +1 -1
  421. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  422. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  423. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  424. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  425. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  426. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  427. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  428. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  429. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  430. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  431. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  432. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  433. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  434. package/dist/modules/settings/connection-check.d.ts +118 -0
  435. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  436. package/dist/modules/settings/connection-check.js +287 -0
  437. package/dist/modules/settings/connection-check.js.map +1 -0
  438. package/dist/modules/settings/deployment-settings.service.d.ts +157 -2
  439. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  440. package/dist/modules/settings/deployment-settings.service.js +412 -35
  441. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  442. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  443. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  444. package/dist/modules/settings/git-root-folders.js +130 -0
  445. package/dist/modules/settings/git-root-folders.js.map +1 -0
  446. package/dist/modules/settings/oidc-check.d.ts +85 -0
  447. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  448. package/dist/modules/settings/oidc-check.js +172 -0
  449. package/dist/modules/settings/oidc-check.js.map +1 -0
  450. package/dist/modules/settings/setup.routes.d.ts +57 -1
  451. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  452. package/dist/modules/settings/setup.routes.js +408 -108
  453. package/dist/modules/settings/setup.routes.js.map +1 -1
  454. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  455. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  456. package/dist/modules/skills/allowed-tools-check.js +260 -0
  457. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  458. package/dist/modules/skills/index.d.ts +4 -1
  459. package/dist/modules/skills/index.d.ts.map +1 -1
  460. package/dist/modules/skills/index.js +2 -0
  461. package/dist/modules/skills/index.js.map +1 -1
  462. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  463. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  464. package/dist/modules/skills/pending-skills.service.js +24 -71
  465. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  466. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  467. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  468. package/dist/modules/skills/skill-access-requests.routes.js +171 -0
  469. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  470. package/dist/modules/skills/skills.contract.d.ts +67 -2
  471. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  472. package/dist/modules/skills/skills.routes.d.ts +35 -3
  473. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  474. package/dist/modules/skills/skills.routes.js +57 -4
  475. package/dist/modules/skills/skills.routes.js.map +1 -1
  476. package/dist/modules/skills/skills.service.d.ts +31 -3
  477. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  478. package/dist/modules/skills/skills.service.js +236 -65
  479. package/dist/modules/skills/skills.service.js.map +1 -1
  480. package/dist/modules/skills/skills.tools.d.ts +4 -1
  481. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  482. package/dist/modules/skills/skills.tools.js +41 -6
  483. package/dist/modules/skills/skills.tools.js.map +1 -1
  484. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  485. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  486. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  487. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  488. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  489. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  490. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  491. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  492. package/dist/modules/tool-auth/external-api-key.interface.d.ts +66 -1
  493. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  494. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  495. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  496. package/dist/modules/tool-auth/external-api-key.service.d.ts +23 -4
  497. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  498. package/dist/modules/tool-auth/external-api-key.service.js +79 -19
  499. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  500. package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
  501. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  502. package/dist/modules/tool-auth/internal-token.service.js +5 -2
  503. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  504. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  505. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  506. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  507. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  508. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
  509. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  510. package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
  511. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  512. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  513. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  514. package/dist/modules/tool-helpers/tool-context.js +28 -6
  515. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  516. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  517. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  518. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  519. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  520. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  521. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  522. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  523. package/dist/modules/tool-manuals/index.d.ts +2 -1
  524. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  525. package/dist/modules/tool-manuals/index.js +1 -0
  526. package/dist/modules/tool-manuals/index.js.map +1 -1
  527. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  528. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  529. package/dist/modules/tool-manuals/mcp-json-discovery.js +136 -96
  530. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  531. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  532. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  533. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  534. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  535. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  536. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  537. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  538. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  539. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  540. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  541. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  542. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  543. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  544. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  545. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  546. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  547. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  548. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  549. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  550. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  551. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +121 -2
  552. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  553. package/dist/modules/tool-manuals/tool-manuals.service.js +415 -92
  554. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  555. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  556. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  557. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  558. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  559. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  560. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  561. package/dist/modules/workflow/event-bus.d.ts +10 -4
  562. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  563. package/dist/modules/workflow/event-bus.js +14 -11
  564. package/dist/modules/workflow/event-bus.js.map +1 -1
  565. package/dist/modules/workflow/events.routes.d.ts +17 -2
  566. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  567. package/dist/modules/workflow/events.routes.js +31 -12
  568. package/dist/modules/workflow/events.routes.js.map +1 -1
  569. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  570. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  571. package/dist/modules/workflow/file-lock.service.js +27 -4
  572. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  573. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  574. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  575. package/dist/modules/workflow/git/change-request-link.js +34 -0
  576. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  577. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  578. package/dist/modules/workflow/git/git-version.js +3 -1
  579. package/dist/modules/workflow/git/git-version.js.map +1 -1
  580. package/dist/modules/workflow/git/git.service.d.ts +303 -6
  581. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  582. package/dist/modules/workflow/git/git.service.js +753 -170
  583. package/dist/modules/workflow/git/git.service.js.map +1 -1
  584. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  585. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  586. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  587. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  588. package/dist/modules/workflow/git/pull-request.service.d.ts +57 -2
  589. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  590. package/dist/modules/workflow/git/pull-request.service.js +165 -25
  591. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  592. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  593. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  594. package/dist/modules/workflow/pending-commits.service.js +24 -4
  595. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  596. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  597. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  598. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  599. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  600. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  601. package/dist/modules/workflow/recovery-bot.js +3 -1
  602. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  603. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  604. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  605. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  606. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  607. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  608. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  609. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  610. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  611. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  612. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  613. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  614. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  615. package/dist/modules/workflow/sanitize-error.js +8 -4
  616. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  617. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  618. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  619. package/dist/modules/workflow/workflow.routes.js +259 -29
  620. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  621. package/dist/modules/workflow/workflow.service.d.ts +217 -14
  622. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  623. package/dist/modules/workflow/workflow.service.js +1142 -109
  624. package/dist/modules/workflow/workflow.service.js.map +1 -1
  625. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  626. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  627. package/dist/modules/workspace/empty-dirs.js +90 -0
  628. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  629. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  630. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  631. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  632. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  633. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  634. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  635. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  636. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  637. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  638. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  639. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  640. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  641. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  642. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  643. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  644. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  645. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  646. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  647. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  648. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  649. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  650. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  651. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  652. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  653. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  654. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  655. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  656. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  657. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  658. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  659. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  660. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  661. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  662. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  663. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  664. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  665. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  666. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  667. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  668. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  669. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  670. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  671. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  672. package/dist/modules/workspace/not-found.d.ts +80 -0
  673. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  674. package/dist/modules/workspace/not-found.js +112 -0
  675. package/dist/modules/workspace/not-found.js.map +1 -0
  676. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  677. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  678. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  679. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  680. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  681. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  682. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  683. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  684. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  685. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  686. package/dist/modules/workspace/startup/kb-git.js +34 -35
  687. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  688. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  689. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  690. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  691. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  692. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  693. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  694. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  695. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  696. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  697. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  698. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  699. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  700. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  701. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  702. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  703. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  704. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  705. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  706. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  707. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  708. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +41 -0
  709. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  710. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +85 -0
  711. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  712. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  713. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  714. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  715. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  716. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  717. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  718. package/dist/modules/workspace/startup/steps/seed-tree.js +174 -54
  719. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  720. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  721. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  722. package/dist/modules/workspace/startup/steps/template-files.step.js +521 -126
  723. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  724. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  725. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  726. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  727. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  728. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  729. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  730. package/dist/modules/workspace/workspace.routes.js +657 -246
  731. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  732. package/dist/modules/workspace/workspace.service.d.ts +323 -12
  733. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  734. package/dist/modules/workspace/workspace.service.js +1006 -257
  735. package/dist/modules/workspace/workspace.service.js.map +1 -1
  736. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  737. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  738. package/dist/modules/workspace/workspace.tools.js +1590 -146
  739. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  740. package/dist/modules/workspace/write-denial.d.ts +76 -0
  741. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  742. package/dist/modules/workspace/write-denial.js +138 -0
  743. package/dist/modules/workspace/write-denial.js.map +1 -0
  744. package/dist/shared/canonical-file-identity.d.ts +64 -0
  745. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  746. package/dist/shared/canonical-file-identity.js +92 -0
  747. package/dist/shared/canonical-file-identity.js.map +1 -0
  748. package/dist/shared/domain-errors.d.ts +128 -0
  749. package/dist/shared/domain-errors.d.ts.map +1 -1
  750. package/dist/shared/domain-errors.js +166 -0
  751. package/dist/shared/domain-errors.js.map +1 -1
  752. package/dist/shared/email-identity.d.ts +32 -0
  753. package/dist/shared/email-identity.d.ts.map +1 -0
  754. package/dist/shared/email-identity.js +37 -0
  755. package/dist/shared/email-identity.js.map +1 -0
  756. package/dist/shared/fs-errors.d.ts +8 -0
  757. package/dist/shared/fs-errors.d.ts.map +1 -0
  758. package/dist/shared/fs-errors.js +11 -0
  759. package/dist/shared/fs-errors.js.map +1 -0
  760. package/dist/shared/fs-walk.d.ts +20 -6
  761. package/dist/shared/fs-walk.d.ts.map +1 -1
  762. package/dist/shared/fs-walk.js +35 -27
  763. package/dist/shared/fs-walk.js.map +1 -1
  764. package/dist/shared/fs.contract.d.ts +206 -0
  765. package/dist/shared/fs.contract.d.ts.map +1 -0
  766. package/dist/shared/fs.contract.js +79 -0
  767. package/dist/shared/fs.contract.js.map +1 -0
  768. package/dist/shared/git-failure.d.ts +80 -0
  769. package/dist/shared/git-failure.d.ts.map +1 -0
  770. package/dist/shared/git-failure.js +139 -0
  771. package/dist/shared/git-failure.js.map +1 -0
  772. package/dist/shared/git-internals.d.ts +17 -0
  773. package/dist/shared/git-internals.d.ts.map +1 -0
  774. package/dist/shared/git-internals.js +123 -0
  775. package/dist/shared/git-internals.js.map +1 -0
  776. package/dist/shared/git.contract.d.ts +157 -0
  777. package/dist/shared/git.contract.d.ts.map +1 -0
  778. package/dist/shared/git.contract.js +103 -0
  779. package/dist/shared/git.contract.js.map +1 -0
  780. package/dist/shared/http-errors.d.ts +33 -0
  781. package/dist/shared/http-errors.d.ts.map +1 -0
  782. package/dist/shared/http-errors.js +35 -0
  783. package/dist/shared/http-errors.js.map +1 -0
  784. package/dist/shared/kb-walk.d.ts +52 -0
  785. package/dist/shared/kb-walk.d.ts.map +1 -0
  786. package/dist/shared/kb-walk.js +76 -0
  787. package/dist/shared/kb-walk.js.map +1 -0
  788. package/dist/shared/logger.contract.d.ts +63 -0
  789. package/dist/shared/logger.contract.d.ts.map +1 -0
  790. package/dist/shared/logger.contract.js +126 -0
  791. package/dist/shared/logger.contract.js.map +1 -0
  792. package/dist/shared/logging.d.ts +31 -0
  793. package/dist/shared/logging.d.ts.map +1 -0
  794. package/dist/shared/logging.js +92 -0
  795. package/dist/shared/logging.js.map +1 -0
  796. package/dist/shared/path-containment.d.ts +21 -0
  797. package/dist/shared/path-containment.d.ts.map +1 -0
  798. package/dist/shared/path-containment.js +34 -0
  799. package/dist/shared/path-containment.js.map +1 -0
  800. package/dist/shared/path-order.d.ts +18 -0
  801. package/dist/shared/path-order.d.ts.map +1 -0
  802. package/dist/shared/path-order.js +35 -0
  803. package/dist/shared/path-order.js.map +1 -0
  804. package/dist/shared/pending-proposals.d.ts +71 -0
  805. package/dist/shared/pending-proposals.d.ts.map +1 -0
  806. package/dist/shared/pending-proposals.js +98 -0
  807. package/dist/shared/pending-proposals.js.map +1 -0
  808. package/dist/shared/printable.d.ts +22 -0
  809. package/dist/shared/printable.d.ts.map +1 -0
  810. package/dist/shared/printable.js +65 -0
  811. package/dist/shared/printable.js.map +1 -0
  812. package/dist/shared/redact-secret.d.ts +30 -0
  813. package/dist/shared/redact-secret.d.ts.map +1 -0
  814. package/dist/shared/redact-secret.js +114 -0
  815. package/dist/shared/redact-secret.js.map +1 -0
  816. package/dist/shared/rename-no-replace.d.ts +119 -0
  817. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  818. package/dist/shared/rename-no-replace.js +323 -0
  819. package/dist/shared/rename-no-replace.js.map +1 -0
  820. package/dist/shared/ssrf.d.ts.map +1 -1
  821. package/dist/shared/ssrf.js +49 -18
  822. package/dist/shared/ssrf.js.map +1 -1
  823. package/dist/shared/ttl-cache.d.ts +28 -1
  824. package/dist/shared/ttl-cache.d.ts.map +1 -1
  825. package/dist/shared/ttl-cache.js +33 -1
  826. package/dist/shared/ttl-cache.js.map +1 -1
  827. package/dist/version.d.ts +10 -6
  828. package/dist/version.d.ts.map +1 -1
  829. package/dist/version.js +10 -6
  830. package/dist/version.js.map +1 -1
  831. package/kb-template/.bevelignore +4 -3
  832. package/kb-template/AGENTS.md +364 -54
  833. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  834. package/kb-template/Skills/.gitkeep +0 -0
  835. package/kb-template/access.md +5 -3
  836. package/kb-template/mcp-description.md +27 -0
  837. package/migrations/0005_github_facade.sql +25 -0
  838. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  839. package/migrations/0007_github_facade_registered.sql +1 -0
  840. package/migrations/0008_change_request_apply_failure.sql +4 -0
  841. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  842. package/migrations/0010_plugin_join_requests.sql +15 -0
  843. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  844. package/migrations/meta/0005_snapshot.json +1717 -0
  845. package/migrations/meta/0006_snapshot.json +1723 -0
  846. package/migrations/meta/0007_snapshot.json +1729 -0
  847. package/migrations/meta/0008_snapshot.json +1753 -0
  848. package/migrations/meta/0009_snapshot.json +1759 -0
  849. package/migrations/meta/0010_snapshot.json +1872 -0
  850. package/migrations/meta/0011_snapshot.json +1884 -0
  851. package/migrations/meta/_journal.json +49 -0
  852. package/package.json +8 -7
  853. package/src/__tests__/core-config.domain.test.ts +67 -2
  854. package/src/__tests__/kb-layout-config.test.ts +295 -0
  855. package/src/__tests__/open-change-gate.ts +15 -0
  856. package/src/core/__tests__/catalog-cache-invalidation.test.ts +226 -0
  857. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  858. package/src/core/__tests__/lifecycle.test.ts +369 -0
  859. package/src/core/__tests__/public-config.test.ts +30 -0
  860. package/src/core/__tests__/readiness.test.ts +109 -0
  861. package/src/core/catalog-cache-invalidation.ts +111 -0
  862. package/src/core/catalog-revision.ts +110 -0
  863. package/src/core/create-core-server.ts +358 -63
  864. package/src/core/create-core-services.ts +449 -107
  865. package/src/core/lifecycle.ts +312 -0
  866. package/src/core/public-config.ts +86 -0
  867. package/src/core/readiness.ts +123 -0
  868. package/src/core-config.ts +70 -4
  869. package/src/index.ts +15 -0
  870. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  871. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  872. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  873. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  874. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  875. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  876. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  877. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +88 -0
  878. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  879. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  880. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  881. package/src/modules/access/__tests__/access-control.service.test.ts +142 -81
  882. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  883. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  884. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  885. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  886. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  887. package/src/modules/access/__tests__/access-plugin-principals.test.ts +385 -0
  888. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  889. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  890. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  891. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  892. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  893. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  894. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  895. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  896. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  897. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  898. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  899. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  900. package/src/modules/access/__tests__/groups-admin.service.test.ts +14 -5
  901. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  902. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  903. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  904. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  905. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  906. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  907. package/src/modules/access/access-control.interface.ts +269 -18
  908. package/src/modules/access/access-control.service.ts +943 -226
  909. package/src/modules/access/access-mutation.service.ts +182 -13
  910. package/src/modules/access/access-view.ts +316 -0
  911. package/src/modules/access/access.routes.ts +385 -105
  912. package/src/modules/access/admin-locked-commit.ts +12 -14
  913. package/src/modules/access/admin-route-helpers.ts +4 -4
  914. package/src/modules/access/change-read-gate.ts +148 -0
  915. package/src/modules/access/creator-access.ts +89 -124
  916. package/src/modules/access/directory-sync-bot.ts +4 -1
  917. package/src/modules/access/groups-admin.service.ts +3 -3
  918. package/src/modules/access/reference-scan.ts +5 -5
  919. package/src/modules/access/roles-admin.service.ts +94 -15
  920. package/src/modules/access/synced-groups-committer.ts +10 -15
  921. package/src/modules/access/synced-groups-writer.ts +3 -12
  922. package/src/modules/access/user-access-removal.service.ts +711 -0
  923. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  924. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  925. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  926. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  927. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  928. package/src/modules/access-model/access-errors.ts +56 -15
  929. package/src/modules/access-model/access-grammar.ts +198 -49
  930. package/src/modules/access-model/access-splice.ts +18 -18
  931. package/src/modules/access-model/access-template.ts +44 -0
  932. package/src/modules/access-model/change-gate.ts +80 -0
  933. package/src/modules/access-model/creator.ts +18 -21
  934. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  935. package/src/modules/access-model/group-files.ts +4 -0
  936. package/src/modules/access-model/plugin-principals.ts +130 -0
  937. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  938. package/src/modules/admin/admin-access.service.ts +6 -2
  939. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  940. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  941. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  942. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  943. package/src/modules/agent-instructions/compose.ts +271 -0
  944. package/src/modules/agent-instructions/index.ts +14 -0
  945. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  946. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  947. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  948. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  949. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  950. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  951. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  952. package/src/modules/auth/account-erasure.service.ts +117 -10
  953. package/src/modules/auth/account.routes.ts +115 -14
  954. package/src/modules/auth/auth.routes.ts +20 -7
  955. package/src/modules/auth/auth.service.ts +187 -28
  956. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  957. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  958. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  959. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  960. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  961. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  962. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  963. package/src/modules/database/advisory-lock.ts +346 -0
  964. package/src/modules/database/connection.ts +9 -1
  965. package/src/modules/database/core-schema.ts +177 -0
  966. package/src/modules/database/migrate.ts +27 -6
  967. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  968. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  969. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  970. package/src/modules/diff/diff.routes.ts +2 -4
  971. package/src/modules/diff/diff.service.ts +75 -79
  972. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  973. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  974. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  975. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  976. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  977. package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
  978. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  979. package/src/modules/kb-fs/node-fs.ts +165 -0
  980. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  981. package/src/modules/kb-fs/repo-path.ts +213 -3
  982. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  983. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  984. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  985. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  986. package/src/modules/kb-sync/index.ts +19 -0
  987. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  988. package/src/modules/kb-sync/kb-sync.routes.ts +200 -0
  989. package/src/modules/kb-sync/kb-sync.service.ts +204 -0
  990. package/src/modules/kb-sync/sync-auth.ts +118 -0
  991. package/src/modules/kb-sync/sync-payload.ts +143 -0
  992. package/src/modules/marketplace/__tests__/github-facade.test.ts +575 -0
  993. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +180 -0
  994. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  995. package/src/modules/marketplace/git-http.routes.ts +196 -0
  996. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +110 -0
  997. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  998. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +263 -0
  999. package/src/modules/marketplace/github-facade/github-facade.routes.ts +294 -0
  1000. package/src/modules/marketplace/github-facade/github-facade.service.ts +282 -0
  1001. package/src/modules/marketplace/github-facade/index.ts +26 -0
  1002. package/src/modules/marketplace/index.ts +3 -0
  1003. package/src/modules/marketplace/marketplace-repo.service.ts +352 -0
  1004. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  1005. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  1006. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  1007. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  1008. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  1009. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  1010. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
  1011. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  1012. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  1013. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  1014. package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
  1015. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  1016. package/src/modules/mcp/downstream-pool.ts +256 -0
  1017. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  1018. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  1019. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  1020. package/src/modules/mcp/first-call-probe.ts +418 -0
  1021. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  1022. package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
  1023. package/src/modules/mcp/mcp.routes.ts +376 -482
  1024. package/src/modules/mcp/mcp.service.ts +846 -176
  1025. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +8 -2
  1026. package/src/modules/mcp/oauth/oauth-consent.routes.ts +28 -6
  1027. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  1028. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  1029. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +166 -0
  1030. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  1031. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +423 -0
  1032. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  1033. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  1034. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  1035. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +201 -16
  1036. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +284 -0
  1037. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  1038. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +330 -20
  1039. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +456 -0
  1040. package/src/modules/plugins/__tests__/plugins.routes.test.ts +441 -43
  1041. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  1042. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  1043. package/src/modules/plugins/compile/compile-marketplace.ts +500 -0
  1044. package/src/modules/plugins/compile/marketplace-compiler.service.ts +151 -0
  1045. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +365 -0
  1046. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +161 -0
  1047. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +250 -0
  1048. package/src/modules/plugins/discovery/kb-plugin-source.ts +117 -0
  1049. package/src/modules/plugins/discovery/native.source.ts +140 -0
  1050. package/src/modules/plugins/discovery/plugin-source.ts +113 -0
  1051. package/src/modules/plugins/index.ts +25 -2
  1052. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  1053. package/src/modules/plugins/join-request-records.store.ts +353 -0
  1054. package/src/modules/plugins/join-requests.service.ts +7 -4
  1055. package/src/modules/plugins/plugin-links.service.ts +350 -0
  1056. package/src/modules/plugins/plugin-links.ts +187 -0
  1057. package/src/modules/plugins/plugin-provision.service.ts +356 -78
  1058. package/src/modules/plugins/plugin-rename.service.ts +310 -0
  1059. package/src/modules/plugins/plugins.contract.ts +79 -4
  1060. package/src/modules/plugins/plugins.routes.ts +320 -123
  1061. package/src/modules/plugins/plugins.service.ts +184 -57
  1062. package/src/modules/plugins/plugins.tools.ts +88 -0
  1063. package/src/modules/plugins/teams.routes.ts +129 -0
  1064. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  1065. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  1066. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  1067. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  1068. package/src/modules/secrets-vault/index.ts +1 -0
  1069. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  1070. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  1071. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  1072. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  1073. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  1074. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +240 -1
  1075. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  1076. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  1077. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  1078. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1079. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1080. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  1081. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1082. package/src/modules/settings/connection-check.ts +351 -0
  1083. package/src/modules/settings/deployment-settings.service.ts +478 -39
  1084. package/src/modules/settings/git-root-folders.ts +171 -0
  1085. package/src/modules/settings/oidc-check.ts +229 -0
  1086. package/src/modules/settings/setup.routes.ts +480 -110
  1087. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1088. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1089. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1090. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  1091. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1092. package/src/modules/skills/__tests__/skills.service.test.ts +308 -2
  1093. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1094. package/src/modules/skills/index.ts +5 -0
  1095. package/src/modules/skills/pending-skills.service.ts +26 -85
  1096. package/src/modules/skills/skill-access-requests.routes.ts +203 -0
  1097. package/src/modules/skills/skills.contract.ts +67 -3
  1098. package/src/modules/skills/skills.routes.ts +75 -4
  1099. package/src/modules/skills/skills.service.ts +258 -66
  1100. package/src/modules/skills/skills.tools.ts +43 -5
  1101. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1102. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1103. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
  1104. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1105. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1106. package/src/modules/tool-auth/external-api-key.interface.ts +73 -1
  1107. package/src/modules/tool-auth/external-api-key.service.ts +95 -29
  1108. package/src/modules/tool-auth/internal-token.service.ts +5 -2
  1109. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1110. package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
  1111. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1112. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1113. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1114. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1115. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1116. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  1117. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1118. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1119. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1120. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1121. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1122. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +11 -4
  1123. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1124. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1125. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1126. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1127. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1128. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1129. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1130. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1131. package/src/modules/tool-manuals/index.ts +3 -0
  1132. package/src/modules/tool-manuals/mcp-json-discovery.ts +164 -95
  1133. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1134. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1135. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1136. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1137. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1138. package/src/modules/tool-manuals/tool-manuals.service.ts +440 -104
  1139. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1140. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1141. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1142. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1143. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1144. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1145. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1146. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1147. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +3 -1
  1148. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1149. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1150. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1151. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1152. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1153. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +3 -0
  1154. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1155. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +296 -27
  1156. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1157. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1158. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1159. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +133 -2
  1160. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +442 -0
  1161. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1162. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1163. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1164. package/src/modules/workflow/event-bus.ts +26 -28
  1165. package/src/modules/workflow/events.routes.ts +56 -21
  1166. package/src/modules/workflow/file-lock.service.ts +27 -4
  1167. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1168. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1169. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1170. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1171. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1172. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1173. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  1174. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  1175. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1176. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  1177. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1178. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1179. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1180. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1181. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1182. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1183. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1184. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1185. package/src/modules/workflow/git/git-version.ts +5 -2
  1186. package/src/modules/workflow/git/git.service.ts +922 -199
  1187. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1188. package/src/modules/workflow/git/pull-request.service.ts +188 -27
  1189. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1190. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1191. package/src/modules/workflow/recovery-bot.ts +4 -1
  1192. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1193. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1194. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1195. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1196. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1197. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1198. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1199. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1200. package/src/modules/workflow/sanitize-error.ts +9 -4
  1201. package/src/modules/workflow/workflow.routes.ts +273 -29
  1202. package/src/modules/workflow/workflow.service.ts +1341 -174
  1203. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1204. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1205. package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
  1206. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1207. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1208. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1209. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1210. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1211. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1212. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1213. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1214. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1215. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +31 -13
  1216. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1217. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +94 -0
  1218. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  1219. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1220. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +98 -13
  1221. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1222. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1223. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1224. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1225. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1226. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1227. package/src/modules/workspace/empty-dirs.ts +79 -0
  1228. package/src/modules/workspace/file-readers/README.md +32 -0
  1229. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1230. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1231. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1232. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1233. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1234. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1235. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1236. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1237. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1238. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1239. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1240. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1241. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1242. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1243. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1244. package/src/modules/workspace/not-found.ts +119 -0
  1245. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1246. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1247. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1248. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1249. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1250. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1251. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1252. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1405 -48
  1253. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1254. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1255. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1256. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1257. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +90 -0
  1258. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1259. package/src/modules/workspace/startup/steps/seed-tree.ts +177 -54
  1260. package/src/modules/workspace/startup/steps/template-files.step.ts +581 -126
  1261. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1262. package/src/modules/workspace/workspace.routes.ts +691 -269
  1263. package/src/modules/workspace/workspace.service.ts +1071 -292
  1264. package/src/modules/workspace/workspace.tools.ts +1787 -180
  1265. package/src/modules/workspace/write-denial.ts +189 -0
  1266. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1267. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1268. package/src/shared/__tests__/filename.test.ts +0 -0
  1269. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1270. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1271. package/src/shared/__tests__/git-internals.test.ts +115 -0
  1272. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1273. package/src/shared/__tests__/logging.test.ts +185 -0
  1274. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1275. package/src/shared/__tests__/path-order.test.ts +24 -0
  1276. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1277. package/src/shared/__tests__/printable.test.ts +64 -0
  1278. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1279. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1280. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1281. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  1282. package/src/shared/canonical-file-identity.ts +92 -0
  1283. package/src/shared/domain-errors.ts +178 -0
  1284. package/src/shared/email-identity.ts +38 -0
  1285. package/src/shared/fs.contract.ts +225 -0
  1286. package/src/shared/git-failure.ts +196 -0
  1287. package/src/shared/git-internals.ts +124 -0
  1288. package/src/shared/git.contract.ts +170 -0
  1289. package/src/shared/http-errors.ts +36 -0
  1290. package/src/shared/logger.contract.ts +141 -0
  1291. package/src/shared/logging.ts +96 -0
  1292. package/src/shared/path-containment.ts +34 -0
  1293. package/src/shared/path-order.ts +33 -0
  1294. package/src/shared/pending-proposals.ts +161 -0
  1295. package/src/shared/printable.ts +68 -0
  1296. package/src/shared/redact-secret.ts +118 -0
  1297. package/src/shared/rename-no-replace.ts +386 -0
  1298. package/src/shared/ssrf.ts +44 -16
  1299. package/src/shared/ttl-cache.ts +33 -1
  1300. package/src/version.ts +10 -6
  1301. package/src/modules/diff/diff-paths.ts +0 -9
  1302. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1303. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1304. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1305. package/src/shared/__tests__/fs-walk.test.ts +0 -42
  1306. package/src/shared/fs-walk.ts +0 -30
  1307. package/src/shared/hash-email.ts +0 -14
@@ -1,13 +1,18 @@
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';
4
+ const log = logger('access');
5
+ import { isAbsence } from '../../shared/fs.contract.js';
6
+ import { isGitTimeout } from '../../shared/git.contract.js';
7
+ import { hashEmail } from '../../shared/email-identity.js';
8
+ import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
9
+ import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, isPersonalPluginDir, pluginIdentityOf, platformRestoreDestination, } from '@bevel-software/platform-shared';
6
10
  import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
11
+ import { WorkflowDomainError } from '../../shared/domain-errors.js';
12
+ import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
7
13
  import { GROUPS_YAML, SYNCED_GROUPS_YAML, parseGroupsFile, } from '../access-model/group-files.js';
8
- import { ADMIN_CANONICAL, EVERYONE_CANONICAL, EVERYONE_DISPLAY, KNOWN_VERBS, ROLE_TOKEN_PREFIX, accessMdSelfEntries, canonicalEmail, canonicalRoleName, isAccessMdPath, mergeGroupsIntoRoles, parseAccessFile, parseOwnAccessEntries, parseRolesYaml, sourceVerbsFor, } from '../access-model/access-grammar.js';
14
+ import { ADMIN_CANONICAL, EVERYONE_CANONICAL, EVERYONE_DISPLAY, KNOWN_VERBS, ROLE_TOKEN_PREFIX, accessMdSelfEntries, canonicalEmail, canonicalRoleName, isAccessMdPath, mergeGroupsIntoRoles, parseAccessFile, parseOwnAccessEntries, parseRolesYaml, requiredVerbsFor, sourceVerbsFor, } from '../access-model/access-grammar.js';
9
15
  import { DIRECTORY_SYNC_BOT_EMAIL, DIRECTORY_SYNC_BOT_NAME, } from './directory-sync-bot.js';
10
- const execFileAsync = promisify(execFile);
11
16
  /**
12
17
  * Read many objects from a git repo in ONE `git cat-file --batch` process.
13
18
  * `specs` are `<ref>:<path>` lines; the result array is index-aligned with
@@ -16,61 +21,36 @@ const execFileAsync = promisify(execFile);
16
21
  * request: `<oid> <type> <size>\n<size bytes>\n`, or `<spec> missing\n`
17
22
  * (the spec may itself contain spaces, hence the endsWith checks).
18
23
  */
19
- function catFileBatch(repoDir, specs) {
20
- return new Promise((resolve, reject) => {
21
- const child = spawn('git', ['-C', repoDir, 'cat-file', '--batch'], {
22
- stdio: ['pipe', 'pipe', 'pipe'],
23
- });
24
- const chunks = [];
25
- const errChunks = [];
26
- child.stdout.on('data', (c) => chunks.push(c));
27
- child.stderr.on('data', (c) => errChunks.push(c));
28
- // A dying git can close stdin mid-write; the 'close' handler below still
29
- // fires with the exit code, which is the error we want to surface.
30
- child.stdin.on('error', () => undefined);
31
- child.on('error', reject);
32
- child.on('close', (code) => {
33
- if (code !== 0) {
34
- reject(new Error(`git cat-file --batch exited ${code}: ${Buffer.concat(errChunks).toString('utf-8').trim()}`));
35
- return;
36
- }
37
- try {
38
- const out = Buffer.concat(chunks);
39
- const results = [];
40
- let off = 0;
41
- for (let i = 0; i < specs.length; i++) {
42
- const nl = out.indexOf(0x0a, off);
43
- if (nl < 0)
44
- throw new Error('unexpected end of git cat-file output');
45
- const header = out.subarray(off, nl).toString('utf-8');
46
- off = nl + 1;
47
- if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
48
- results.push(null);
49
- continue;
50
- }
51
- const parts = header.split(' ');
52
- const size = Number(parts[2]);
53
- if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
54
- throw new Error(`unexpected git cat-file header: ${header}`);
55
- }
56
- // Non-blob (a directory path resolves to a tree) → null, but the
57
- // payload still has to be skipped to stay aligned.
58
- results.push(parts[1] === 'blob' ? out.subarray(off, off + size).toString('utf-8') : null);
59
- off += size + 1; // payload + trailing LF
60
- }
61
- resolve(results);
62
- }
63
- catch (err) {
64
- reject(err);
65
- }
66
- });
67
- child.stdin.write(`${specs.join('\n')}\n`);
68
- child.stdin.end();
24
+ async function catFileBatch(runner, repoDir, specs) {
25
+ // Bytes, not text: every `<size>` in the header counts bytes, and the walk
26
+ // below has to step by that count through content that is not all ASCII.
27
+ const { stdout: out } = await runner.run(repoDir, ['cat-file', '--batch'], {
28
+ input: `${specs.join('\n')}\n`,
29
+ encoding: 'buffer',
69
30
  });
70
- }
71
- /** Mirrors `shared/hash-email.ts`. Duplicated to avoid a cross-cutting import. */
72
- function sha256Email(email) {
73
- return createHash('sha256').update(email.trim().toLowerCase()).digest('hex');
31
+ const results = [];
32
+ let off = 0;
33
+ for (let i = 0; i < specs.length; i++) {
34
+ const nl = out.indexOf(0x0a, off);
35
+ if (nl < 0)
36
+ throw new Error('unexpected end of git cat-file output');
37
+ const header = out.subarray(off, nl).toString('utf-8');
38
+ off = nl + 1;
39
+ if (header.endsWith(' missing') || header.endsWith(' ambiguous')) {
40
+ results.push(null);
41
+ continue;
42
+ }
43
+ const parts = header.split(' ');
44
+ const size = Number(parts[2]);
45
+ if (parts.length !== 3 || !Number.isInteger(size) || size < 0) {
46
+ throw new Error(`unexpected git cat-file header: ${header}`);
47
+ }
48
+ // Non-blob (a directory path resolves to a tree) → null, but the
49
+ // payload still has to be skipped to stay aligned.
50
+ results.push(parts[1] === 'blob' ? out.subarray(off, off + size).toString('utf-8') : null);
51
+ off += size + 1; // payload + trailing LF
52
+ }
53
+ return results;
74
54
  }
75
55
  function isBuiltInRole(canonicalRole) {
76
56
  return canonicalRole === EVERYONE_CANONICAL;
@@ -78,11 +58,6 @@ function isBuiltInRole(canonicalRole) {
78
58
  function roleKnown(roles, canonicalRole) {
79
59
  return roles.byCanonical.has(canonicalRole) || isBuiltInRole(canonicalRole);
80
60
  }
81
- /** True for the errno codes that mean "the file genuinely is not there". */
82
- function isAbsenceError(err) {
83
- const code = err?.code;
84
- return code === 'ENOENT' || code === 'ENOTDIR';
85
- }
86
61
  /**
87
62
  * Load the ACTIVE group source through `read` (working tree or at-ref — the
88
63
  * caller supplies the reader, so both model loaders share one mode rule):
@@ -90,13 +65,13 @@ function isAbsenceError(err) {
90
65
  * even when the synced file is empty or malformed — falling back would
91
66
  * resurrect retired manual groups); otherwise `groups.yaml` → manual mode.
92
67
  *
93
- * `read` returns null for a genuinely-absent file and may THROW for any other
94
- * failure. Only ENOENT/ENOTDIR count as absent (the caller may also whitelist
95
- * them into null); every other read error — notably a non-absence error on
96
- * `synced-groups.yaml` — is treated as a BROKEN source: no groups, no
97
- * fallback to the manual file (falling back would resurrect retired groups),
98
- * and an `ok: false` health marker. Structural parse failures degrade the
99
- * same way; this function never throws.
68
+ * `read` returns null for a genuinely-absent file — ENOENT/ENOTDIR, the
69
+ * reader's judgement through the fs probe — and THROWS for any other failure.
70
+ * Every thrown error — notably one on `synced-groups.yaml` — is treated as a
71
+ * BROKEN source: no groups, no fallback to the manual file (falling back
72
+ * would resurrect retired groups), and an `ok: false` health marker.
73
+ * Structural parse failures degrade the same way; this function never throws
74
+ * (except to let an `AccessUnreadableError` through — see below).
100
75
  */
101
76
  export async function loadActiveGroups(read) {
102
77
  const broken = (file, reason) => ({
@@ -119,15 +94,10 @@ export async function loadActiveGroups(read) {
119
94
  // the same error; groups cannot be the one input that does not.
120
95
  if (err instanceof AccessUnreadableError)
121
96
  throw err;
122
- if (isAbsenceError(err)) {
123
- syncedText = null;
124
- }
125
- else {
126
- // A non-absence read error on the SYNCED source must NOT fall back to
127
- // groups.yaml — the synced file may exist and its manual predecessor is
128
- // retired. Broken-groups instead.
129
- return broken(SYNCED_GROUPS_YAML, err instanceof Error ? err.message : String(err));
130
- }
97
+ // A read error on the SYNCED source must NOT fall back to groups.yaml —
98
+ // the synced file may exist and its manual predecessor is retired.
99
+ // Broken-groups instead.
100
+ return broken(SYNCED_GROUPS_YAML, err instanceof Error ? err.message : String(err));
131
101
  }
132
102
  const sourceFile = syncedText !== null ? SYNCED_GROUPS_YAML : GROUPS_YAML;
133
103
  let text;
@@ -141,10 +111,7 @@ export async function loadActiveGroups(read) {
141
111
  catch (err) {
142
112
  if (err instanceof AccessUnreadableError)
143
113
  throw err; // see above
144
- if (isAbsenceError(err))
145
- text = null;
146
- else
147
- return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
114
+ return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
148
115
  }
149
116
  }
150
117
  if (text === null)
@@ -165,17 +132,84 @@ export async function loadActiveGroups(read) {
165
132
  * `hasPermissionResolved`). `collapseScopes` flattens this into the
166
133
  * closest-wins-per-principal view the display helpers use.
167
134
  */
135
+ /**
136
+ * The rules listener on the one walk of the checkout: every `access.md` at
137
+ * any depth (the access tree is structure-agnostic — the root's included),
138
+ * plus every plugin manifest under the plugins root, for principal
139
+ * synthesis. A reader: a folder the walk could not list is simply not in
140
+ * the model (a writer that needs completeness asks the walk for its holes).
141
+ *
142
+ * Per-file failures (malformed YAML, bad role refs, unknown verbs) are
143
+ * logged and the offending file is dropped from the model — they do NOT
144
+ * throw. A typo in one nested access.md must not 500 the entire editor;
145
+ * admins can still write `access.md` / `roles.yaml` because
146
+ * `hasPermissionResolved` admin-rescues those paths, so the bad config
147
+ * remains fixable from inside the app.
148
+ */
149
+ function accessFileListener(repoDir, out, pluginDirs) {
150
+ return {
151
+ async onFile(dir, name) {
152
+ const rel = dir ? `${dir}/${name}` : name;
153
+ if (name === PLUGIN_MANIFEST_FILE && dir.startsWith(`${PLUGINS_DIR}/`)) {
154
+ const text = await fs.readFile(path.join(repoDir, rel), 'utf-8').catch(() => null);
155
+ pluginDirs.set(dir, pluginIdentityOf(parseManifestText(text), path.posix.basename(dir)));
156
+ return;
157
+ }
158
+ if (name !== 'access.md')
159
+ return;
160
+ const text = await fs.readFile(path.join(repoDir, rel), 'utf-8');
161
+ const parsed = parseAccessFile(text, rel);
162
+ if (!parsed.ok) {
163
+ // Treat as if the file didn't exist for resolution purposes.
164
+ // Admin-rescue on access.md paths still lets an admin fix it.
165
+ for (const e of parsed.errors)
166
+ log.warn(`${e} — file ignored`);
167
+ return;
168
+ }
169
+ for (const w of parsed.warnings)
170
+ log.warn(w);
171
+ if (out.has(parsed.file.dir)) {
172
+ log.warn(`${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
173
+ return;
174
+ }
175
+ out.set(parsed.file.dir, parsed.file);
176
+ },
177
+ };
178
+ }
179
+ /** A manifest's parsed object, or null for absent, unparsable or not-an-object text. */
180
+ function parseManifestText(text) {
181
+ if (text === null)
182
+ return null;
183
+ try {
184
+ const parsed = JSON.parse(text);
185
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
186
+ ? parsed
187
+ : null;
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
168
193
  function resolveScopes(model, verb, relativePath, fileOwn) {
169
194
  const target = verb;
195
+ // Both lists come from the grammar's one dependency graph (`VERB_REQUIRES`):
196
+ // the verbs whose grant confers the target, and the verbs the target
197
+ // presupposes, whose denial strips it.
170
198
  const supersets = sourceVerbsFor(verb).filter((v) => v !== verb);
199
+ const prerequisites = requiredVerbsFor(verb).filter((v) => v !== verb);
171
200
  // Build one scope's effective verdicts. The target verb contributes both
172
- // grants and denials; superset verbs contribute grants only (a `deny write`
173
- // never strips `read`). Within the scope a grant always wins over a deny of
174
- // the same principal, so a same-file superset grant overrides a target deny
175
- // (e.g. `owner:` beats `deny write`, `write:` beats `deny read`).
201
+ // grants and denials. Superset verbs contribute grants only (a `deny write`
202
+ // never strips `read`); prerequisite verbs contribute DENIALS only (a `deny
203
+ // write` strips `owner`, a `deny read` strips all three — nobody owns what
204
+ // they may not edit, or edits what they may not open), and a `read` grant
205
+ // confers no `write`. Within the scope a grant always wins over a deny of
206
+ // the same principal, so a same-file grant of the target or of a superset
207
+ // overrides a deny of the target or of a prerequisite (`owner:` beats `deny
208
+ // write`, `write:` beats `deny read`).
176
209
  const buildScope = (entries, filterRoles, source) => {
177
210
  const byRole = new Map();
178
211
  const byEmail = new Map();
212
+ const implied = { byRole: new Set(), byEmail: new Set() };
179
213
  const set = (entry, state) => {
180
214
  const map = entry.kind === 'role' ? byRole : byEmail;
181
215
  const key = entry.kind === 'role' ? entry.role : entry.email;
@@ -183,8 +217,9 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
183
217
  return; // a grant in this scope sticks
184
218
  map.set(key, state);
185
219
  };
220
+ const skip = (entry) => filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role);
186
221
  for (const entry of entries[target]) {
187
- if (filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role))
222
+ if (skip(entry))
188
223
  continue;
189
224
  set(entry, entry.deny ? 'denied' : 'grant');
190
225
  }
@@ -192,12 +227,53 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
192
227
  for (const entry of entries[src]) {
193
228
  if (entry.deny)
194
229
  continue; // grant-only fold
195
- if (filterRoles && entry.kind === 'role' && !roleKnown(model.roles, entry.role))
230
+ if (skip(entry))
196
231
  continue;
197
232
  set(entry, 'grant');
198
233
  }
199
234
  }
200
- return { byRole, byEmail, source };
235
+ // Last, so a target or superset GRANT already in the map stands, and a
236
+ // target's own `deny` line is never re-labelled as implied.
237
+ for (const src of prerequisites) {
238
+ for (const entry of entries[src]) {
239
+ if (!entry.deny)
240
+ continue; // deny-only fold
241
+ if (skip(entry))
242
+ continue;
243
+ const map = entry.kind === 'role' ? byRole : byEmail;
244
+ const key = entry.kind === 'role' ? entry.role : entry.email;
245
+ if (map.has(key))
246
+ continue; // an own verdict (grant or deny) is already the answer
247
+ map.set(key, 'denied');
248
+ (entry.kind === 'role' ? implied.byRole : implied.byEmail).add(key);
249
+ }
250
+ }
251
+ // The anonymous caller's verdict at this scope — what someone who holds
252
+ // exactly the PUBLIC keys (a plugin principal whose plugin admits
253
+ // everyone, so everyone holds it) resolves to, by the same tiers a named
254
+ // caller gets: a grant via any public key wins, else a denial via one
255
+ // denies, else the built-in `everyone` entry decides. Derived HERE, where
256
+ // a scope's verdicts are built, so every reader of it — the permission
257
+ // check, the eligible lists, the `restricted` flag, the compiler's
258
+ // everyone audience — sees one truth; and derived into its own field,
259
+ // never into `byRole`, so the entries a person can remove stay exactly
260
+ // the lines the files hold.
261
+ let publicGrant = false;
262
+ let publicDeny = false;
263
+ for (const [key, state] of byRole) {
264
+ if (!model.roles.publicKeys?.has(key))
265
+ continue;
266
+ if (state === 'grant')
267
+ publicGrant = true;
268
+ else if (state === 'denied')
269
+ publicDeny = true;
270
+ }
271
+ const everyone = publicGrant
272
+ ? 'grant'
273
+ : publicDeny
274
+ ? 'denied'
275
+ : byRole.get(EVERYONE_CANONICAL);
276
+ return { byRole, byEmail, everyone, source, implied };
201
277
  };
202
278
  const scopes = [];
203
279
  // The node's own frontmatter is the most specific scope. Role refs there are
@@ -221,17 +297,124 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
221
297
  function collapseScopes(scopes) {
222
298
  const byRole = new Map();
223
299
  const byEmail = new Map();
300
+ let everyone;
224
301
  for (let i = scopes.length - 1; i >= 0; i--) {
225
302
  for (const [k, v] of scopes[i].byRole)
226
303
  byRole.set(k, v);
227
304
  for (const [k, v] of scopes[i].byEmail)
228
305
  byEmail.set(k, v);
306
+ if (scopes[i].everyone !== undefined)
307
+ everyone = scopes[i].everyone;
229
308
  }
230
- return { byRole, byEmail };
309
+ return { byRole, byEmail, everyone };
231
310
  }
232
311
  function resolveAtPath(model, verb, relativePath, fileOwn) {
233
312
  return collapseScopes(resolveScopes(model, verb, relativePath, fileOwn));
234
313
  }
314
+ /**
315
+ * Every principal key a caller holds: the tokens their email is a member of,
316
+ * plus the PUBLIC keys every caller holds regardless (a plugin principal
317
+ * whose plugin grants `everyone`). Undefined when there is nothing at all,
318
+ * matching the plain `byEmail.get` the tier-2 check used to read.
319
+ */
320
+ function principalKeysOf(model, email) {
321
+ const own = model.roles.byEmail.get(email);
322
+ const pub = model.roles.publicKeys;
323
+ if (!pub?.size)
324
+ return own;
325
+ return new Set([...(own ?? []), ...pub]);
326
+ }
327
+ /** The explicit key of the Admin ROLE — never the bare token, which a same-named group may own. */
328
+ const ADMIN_ROLE_KEY = `${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`;
329
+ /**
330
+ * Add to `keys` what holding them confers on top: every plugin principal
331
+ * expanded from one of the keys, and the public keys every caller holds.
332
+ */
333
+ function addDerivedKeys(model, keys) {
334
+ for (const [key, principal] of model.roles.byCanonical) {
335
+ if (principal.kind !== 'plugin' || !principal.sourceKeys)
336
+ continue;
337
+ for (const source of principal.sourceKeys) {
338
+ if (keys.has(source)) {
339
+ keys.add(key);
340
+ break;
341
+ }
342
+ }
343
+ }
344
+ for (const key of model.roles.publicKeys ?? [])
345
+ keys.add(key);
346
+ return keys;
347
+ }
348
+ /**
349
+ * Every principal key that being in `group` confers — the group's OWN key,
350
+ * the roles that list the group (`group:<Name>` in roles.yaml), the plugin
351
+ * principals whose roster admits the group directly or through one of those
352
+ * roles, and the public keys every caller holds. This is the group-shaped
353
+ * twin of `principalKeysOf`: a member's key set minus everything they hold
354
+ * for reasons of their own. `null` when no group of that name exists.
355
+ *
356
+ * Groups are keyed by their bare canonical name (`mergeGroupsIntoRoles`);
357
+ * roles by `role/<name>`. A bare key whose record is a ROLE is not a group,
358
+ * however it was spelled.
359
+ */
360
+ function principalKeysOfGroup(model, group) {
361
+ const canonical = canonicalRoleName(group);
362
+ const record = model.roles.byCanonical.get(canonical);
363
+ if (!record || record.kind !== 'group')
364
+ return null;
365
+ const keys = new Set([canonical]);
366
+ for (const [key, principal] of model.roles.byCanonical) {
367
+ if (principal.kind === 'role' && principal.groupRefs?.has(canonical))
368
+ keys.add(key);
369
+ }
370
+ // Plugin principals were expanded from role/group entries; a plugin that
371
+ // admits the group, or a role the group is folded into, admits the group.
372
+ return addDerivedKeys(model, keys);
373
+ }
374
+ /** The key set of a caller who holds nothing: `hasPermissionForKeys` then answers as `everyone`. */
375
+ const NO_KEYS = new Set();
376
+ /**
377
+ * The tier-2 half of `hasPermissionResolved` on its own: a verdict for a set
378
+ * of principal KEYS with no person behind them — no direct-email tier, no
379
+ * admin rescue, no machine-owned rule — only the Admin write floor, when the
380
+ * keys include the Admin role. Closest scope first; within a scope a
381
+ * grant to any key wins over a denial to another, exactly as for a person.
382
+ */
383
+ function hasPermissionForKeys(model, verb, keys, relativePath, fileOwn) {
384
+ const adminFloor = verb === 'write' && keys.has(ADMIN_ROLE_KEY);
385
+ for (const scope of resolveScopes(model, verb, relativePath, fileOwn)) {
386
+ if (adminFloor && isAdminFloorScope(scope, relativePath))
387
+ return true;
388
+ let grant = false;
389
+ let deny = false;
390
+ for (const key of keys) {
391
+ const state = scope.byRole.get(key);
392
+ if (state === 'denied')
393
+ deny = true;
394
+ else if (state === 'grant')
395
+ grant = true;
396
+ }
397
+ if (grant)
398
+ return true;
399
+ if (deny)
400
+ return false;
401
+ if (scope.everyone)
402
+ return scope.everyone === 'grant';
403
+ }
404
+ return adminFloor;
405
+ }
406
+ /**
407
+ * Whether a scope sits at the Admin write floor: the repository root's own
408
+ * `access.md`, or the own frontmatter of a file directly in the root (those
409
+ * files are part of the root, so their own rules cannot take Admin's write
410
+ * away either). A folder directly in the root is not covered — its own
411
+ * `access.md` is an ordinary subfolder scope that may exclude Admin.
412
+ */
413
+ function isAdminFloorScope(scope, relativePath) {
414
+ if (scope.source.kind === 'own')
415
+ return !relativePath.includes('/');
416
+ return scope.source.path === 'access.md';
417
+ }
235
418
  function isAdminEmail(model, email) {
236
419
  if (model.deploymentOwners.has(email))
237
420
  return true;
@@ -239,7 +422,7 @@ function isAdminEmail(model, email) {
239
422
  // Check the explicit `role/admin` alias, NOT the bare token: bare-name
240
423
  // precedence is group-first, so a group that happens to be named "Admin"
241
424
  // owns the bare key — and its members must never inherit the capability.
242
- return !!roles && roles.has(`${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`);
425
+ return !!roles && roles.has(ADMIN_ROLE_KEY);
243
426
  }
244
427
  /**
245
428
  * Resolve whether `userEmail` has `verb` on `relativePath`.
@@ -272,48 +455,76 @@ function isAdminEmail(model, email) {
272
455
  * owner is whoever can already set `ADMIN_EMAIL`, so this concedes no
273
456
  * authority they did not have; it only gives it a door.
274
457
  *
458
+ * One floor for `write`, applied during the walk: an admin (same definition)
459
+ * always holds `write` at the repository root. When the walk reaches the root
460
+ * `access.md` (or the own frontmatter of a file directly in the root) — or runs
461
+ * out of scopes without a verdict — an admin is granted,
462
+ * whatever the root file says, so no root rule can lock the deployment out of
463
+ * its own tree. A NEARER scope still decides first: a subfolder that denies
464
+ * Admin (or denies `everyone` without naming Admin) excludes admins there like
465
+ * anyone else. The root file's own entries are not rewritten — the floor is a
466
+ * verdict, not a grant line — so `grantSources` stays file-backed.
467
+ *
275
468
  * No special-cases for `read`/`download` — they fall through to scope resolution.
276
469
  */
277
470
  function hasPermissionResolved(model, verb, userEmail, relativePath, fileOwn) {
471
+ return decidePermissionResolved(model, verb, userEmail, relativePath, fileOwn).allowed;
472
+ }
473
+ function decidePermissionResolved(model, verb, userEmail, relativePath, fileOwn) {
278
474
  const email = canonicalEmail(userEmail);
279
475
  if (verb === 'write') {
280
- if (relativePath === 'roles.yaml')
281
- return isAdminEmail(model, email);
282
- if (isAccessMdPath(relativePath) && isAdminEmail(model, email))
283
- return true;
476
+ if (relativePath === 'roles.yaml') {
477
+ return { allowed: isAdminEmail(model, email), scope: null, via: 'admin-rescue' };
478
+ }
479
+ if (isAccessMdPath(relativePath) && isAdminEmail(model, email)) {
480
+ return { allowed: true, scope: null, via: 'admin-rescue' };
481
+ }
482
+ }
483
+ const adminFloor = verb === 'write' && isAdminEmail(model, email);
484
+ // For write, the deployment owner IS Admin — the floor admits them, so the
485
+ // Admin rules along the way bind them too: a subfolder denying Admin excludes
486
+ // the owner exactly as it excludes the role's members.
487
+ let userRoles = principalKeysOf(model, email);
488
+ if (adminFloor && !userRoles?.has(ADMIN_ROLE_KEY)) {
489
+ userRoles = new Set([...(userRoles ?? []), ...principalKeysOfToken(model, ADMIN_ROLE_KEY)]);
284
490
  }
285
- const userRoles = model.roles.byEmail.get(email);
286
491
  const scopes = resolveScopes(model, verb, relativePath, fileOwn);
287
492
  for (const scope of scopes) {
493
+ if (adminFloor && isAdminFloorScope(scope, relativePath))
494
+ return { allowed: true, scope, via: 'admin-floor' };
288
495
  // Tier 1 — direct email entry is the most specific verdict at this scope.
289
496
  const direct = scope.byEmail.get(email);
290
497
  if (direct)
291
- return direct === 'grant';
498
+ return { allowed: direct === 'grant', scope, via: 'email', key: email };
292
499
  // Tier 2 — the caller's roles. A grant via any one of them wins over a deny
293
500
  // via another at this same scope (a `deny Engineer` doesn't undo an
294
501
  // unrelated `Admin` grant).
295
502
  if (userRoles && userRoles.size) {
296
- let grant = false;
297
- let deny = false;
503
+ let granted;
504
+ let denied;
298
505
  for (const r of userRoles) {
299
506
  const s = scope.byRole.get(r);
300
507
  if (s === 'denied')
301
- deny = true;
508
+ denied ??= r;
302
509
  else if (s === 'grant')
303
- grant = true;
510
+ granted ??= r;
304
511
  }
305
- if (grant)
306
- return true;
307
- if (deny)
308
- return false;
512
+ if (granted !== undefined)
513
+ return { allowed: true, scope, via: 'principal', key: granted };
514
+ if (denied !== undefined)
515
+ return { allowed: false, scope, via: 'principal', key: denied };
516
+ }
517
+ // Tier 3 — the built-in `everyone` role (derived: a public plugin
518
+ // principal granted here counts, see `AccessScope.everyone`).
519
+ if (scope.everyone) {
520
+ return { allowed: scope.everyone === 'grant', scope, via: 'everyone', key: EVERYONE_CANONICAL };
309
521
  }
310
- // Tier 3 — the built-in `everyone` role.
311
- const everyone = scope.byRole.get(EVERYONE_CANONICAL);
312
- if (everyone)
313
- return everyone === 'grant';
314
522
  // No verdict at this scope — fall through to the next (farther) one.
315
523
  }
316
- return false;
524
+ // No verdict anywhere (and no root access.md to stop at): the floor still holds.
525
+ return adminFloor
526
+ ? { allowed: true, scope: null, via: 'admin-floor' }
527
+ : { allowed: false, scope: null, via: 'default-deny' };
317
528
  }
318
529
  /**
319
530
  * The `access.md` that a FOLDER target's own grants live in — its direct scope.
@@ -340,6 +551,39 @@ function scopeToGrantSource(scope, kind, relativePath) {
340
551
  return { kind: 'direct' };
341
552
  return { kind: 'ancestor', path };
342
553
  }
554
+ /**
555
+ * Where a scope's rules live, as the place a person edits: a folder (the
556
+ * directory of its `access.md`) or the target's own frontmatter.
557
+ */
558
+ function decisionSourceOf(scope, kind, relativePath) {
559
+ if (scope.source.kind === 'own')
560
+ return { kind: 'frontmatter', path: relativePath, inherited: false };
561
+ const folder = path.posix.dirname(scope.source.path);
562
+ const dir = folder === '.' ? '' : folder;
563
+ const grant = scopeToGrantSource(scope, kind, relativePath);
564
+ return { kind: 'folder', path: dir, inherited: grant.kind === 'ancestor' };
565
+ }
566
+ /** A resolver decision in the terms `explainAccess` reports. */
567
+ function explainDecision(model, decision, kind, relativePath) {
568
+ const source = decision.scope ? decisionSourceOf(decision.scope, kind, relativePath) : null;
569
+ const base = { allowed: decision.allowed, source };
570
+ switch (decision.via) {
571
+ case 'admin-rescue':
572
+ case 'default-deny':
573
+ return { ...base, via: decision.via, principal: null };
574
+ case 'admin-floor':
575
+ return { ...base, via: decision.via, principal: 'Admin' };
576
+ case 'email':
577
+ return { ...base, via: 'person', principal: decision.key ?? null };
578
+ case 'everyone':
579
+ return { ...base, via: 'everyone', principal: EVERYONE_CANONICAL };
580
+ case 'principal': {
581
+ const key = decision.key ?? '';
582
+ const record = model.roles.byCanonical.get(key);
583
+ return { ...base, via: record?.kind ?? 'role', principal: record?.displayName ?? key };
584
+ }
585
+ }
586
+ }
343
587
  /**
344
588
  * Resolve EVERY file scope that names a principal for `verb`, ordered
345
589
  * closest-first — the source-returning twin of `hasPermissionResolved`, but
@@ -389,13 +633,7 @@ function resolveGrantSourcesForVerb(model, verb, kind, relativePath, principal,
389
633
  // a GROUP whose group has VANISHED reads "unshadowed" here, and the
390
634
  // alias-tolerant pair would then attribute a same-named role's
391
635
  // surviving `role/<name>` grant to the group.
392
- const canonical = canonicalRoleName(principal.role);
393
- const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
394
- const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
395
- const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
396
- const tokens = tokenMatch === 'exact' || shadowed
397
- ? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
398
- : [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
636
+ const tokens = ownTokensOf(model, principal.role, tokenMatch);
399
637
  for (const scope of scopes) {
400
638
  const states = tokens.map((t) => scope.byRole.get(t));
401
639
  if (states.includes('grant')) {
@@ -422,6 +660,69 @@ function resolveGrantSourcesForVerb(model, verb, kind, relativePath, principal,
422
660
  }
423
661
  return out;
424
662
  }
663
+ /**
664
+ * WHICH token spellings in a file are THIS role-shaped principal's own entries
665
+ * — the shadowing rule, in one place because the grant walk, the denial walk
666
+ * and the revoke splice must all agree on it (a disagreement attributes a
667
+ * shadowed bare token to the wrong principal, which is how a "restrict" silently
668
+ * strips a same-named role's grant). See `resolveGrantSourcesForVerb` for the
669
+ * full reasoning behind each branch.
670
+ */
671
+ function ownTokensOf(model, role, tokenMatch) {
672
+ const canonical = canonicalRoleName(role);
673
+ const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
674
+ const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
675
+ const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
676
+ return tokenMatch === 'exact' || shadowed
677
+ ? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
678
+ : [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
679
+ }
680
+ /**
681
+ * The polarity twin of {@link resolveGrantSourcesForVerb}: every file scope
682
+ * whose own entry for this principal DENIES `verb`, closest-first.
683
+ *
684
+ * Same walk, mirrored stopping rule. A grant in the principal's own entry at a
685
+ * scope ENDS the list — under closest-wins a farther deny is dead, and reporting
686
+ * it would have the dialog say "restricted" about a verb the principal holds.
687
+ * A grant or deny reaching the principal through a group/role/`everyone` is not
688
+ * their own entry: it is neither a denial to report nor a reason to stop, so the
689
+ * walk carries on past it, exactly as the grant twin does.
690
+ *
691
+ * Returns `[]` when the principal is not denied `verb` by any file entry.
692
+ */
693
+ function resolveDenialSourcesForVerb(model, verb, kind, relativePath, principal, fileOwn, tokenMatch) {
694
+ const scopes = resolveScopes(model, verb, relativePath, fileOwn);
695
+ const out = [];
696
+ if (principal.kind === 'role') {
697
+ const tokens = ownTokensOf(model, principal.role, tokenMatch);
698
+ for (const scope of scopes) {
699
+ const states = tokens.map((t) => scope.byRole.get(t));
700
+ // A grant under EITHER spelling wins within the scope (`buildScope` lets a
701
+ // grant stick over a deny of the same principal), so it ends the walk
702
+ // whether or not the other spelling denies here.
703
+ if (states.includes('grant'))
704
+ break;
705
+ // Only a `deny <verb>` LINE is a source; a denial the scope implies from
706
+ // a prerequisite verb's line (`deny write` when asking about owner) is
707
+ // real but is not this verb's entry — the prerequisite's own report is
708
+ // where it can be lifted.
709
+ const written = tokens.some((t) => scope.byRole.get(t) === 'denied' && !scope.implied.byRole.has(t));
710
+ if (written)
711
+ out.push(scopeToGrantSource(scope, kind, relativePath));
712
+ }
713
+ return out;
714
+ }
715
+ const email = canonicalEmail(principal.email);
716
+ for (const scope of scopes) {
717
+ const own = scope.byEmail.get(email);
718
+ if (own === 'grant')
719
+ break;
720
+ if (own === 'denied' && !scope.implied.byEmail.has(email)) {
721
+ out.push(scopeToGrantSource(scope, kind, relativePath));
722
+ }
723
+ }
724
+ return out;
725
+ }
425
726
  /**
426
727
  * Build the repo-root → path chain of directory scopes that govern
427
728
  * `relativePath`, e.g. `Knowledge/Sales/Foo.md` →
@@ -454,11 +755,11 @@ function dirChainFor(relativePath) {
454
755
  * reported as restricted.
455
756
  */
456
757
  function canEveryoneReadResolved(model, relativePath, fileOwn) {
457
- const { byRole, byEmail } = resolveAtPath(model, 'read', relativePath, fileOwn);
758
+ const { byEmail, everyone } = resolveAtPath(model, 'read', relativePath, fileOwn);
458
759
  // Baseline: an unnamed signed-in user (no email/role entries) reads only via
459
760
  // `everyone`. As a single principal, its collapsed verdict is its closest —
460
761
  // exactly what that user resolves to.
461
- if (byRole.get(EVERYONE_CANONICAL) !== 'grant')
762
+ if (everyone !== 'grant')
462
763
  return false;
463
764
  // Every *named* principal must also still read. A collapsed deny may have
464
765
  // been shadowed by a closer-scope `everyone` grant, so re-resolve each
@@ -490,8 +791,34 @@ function canEveryoneReadResolved(model, relativePath, fileOwn) {
490
791
  function canReadResolved(model, userEmail, relativePath, fileOwn) {
491
792
  return hasPermissionResolved(model, 'read', userEmail, relativePath, fileOwn);
492
793
  }
794
+ /**
795
+ * The key set someone holding exactly the principal behind `token` carries —
796
+ * the question "would a member of this principal be allowed?" put to
797
+ * `hasPermissionForKeys`. A group gets its full member key set
798
+ * (`principalKeysOfGroup`); a role or plugin principal gets every key its
799
+ * record is registered under (the bare name and the `role/` alias share one
800
+ * record) plus the plugin principals expanded from those keys; a token with no
801
+ * record (`everyone`, a vanished principal) is just itself. Public keys are
802
+ * always included — every caller holds them.
803
+ */
804
+ function principalKeysOfToken(model, token) {
805
+ const record = model.roles.byCanonical.get(token);
806
+ if (record?.kind === 'group')
807
+ return principalKeysOfGroup(model, token) ?? new Set([token]);
808
+ const keys = new Set([token]);
809
+ if (!record) {
810
+ for (const key of model.roles.publicKeys ?? [])
811
+ keys.add(key);
812
+ return keys;
813
+ }
814
+ for (const [key, principal] of model.roles.byCanonical) {
815
+ if (principal === record)
816
+ keys.add(key);
817
+ }
818
+ return addDerivedKeys(model, keys);
819
+ }
493
820
  function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
494
- const { byRole, byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
821
+ const { byRole, byEmail, everyone } = resolveAtPath(model, verb, relativePath, fileOwn);
495
822
  // (kind, display name) → one entry. The `byRole` keys are the merged
496
823
  // index's canonical tokens exactly as granted (bare, or the
497
824
  // `role/<canonical>` alias); the `byCanonical` record a key hits carries
@@ -509,12 +836,34 @@ function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
509
836
  if (!byIdentity.has(key))
510
837
  byIdentity.set(key, { name, kind });
511
838
  };
839
+ // Every row is re-checked against the resolver. The collapsed view keeps a
840
+ // principal's closest OWN verdict, but a nearer scope can still decide for
841
+ // them without naming them — a `deny everyone` in a subfolder cuts off a
842
+ // root `write: Admin` — and a list that named them anyway would tell the
843
+ // dialog and the denial message that someone is eligible whom the gate then
844
+ // refuses.
845
+ const allowedFor = (keys) => hasPermissionForKeys(model, verb, keys, relativePath, fileOwn);
512
846
  for (const [canonical, state] of byRole) {
513
847
  if (state !== 'grant')
514
848
  continue;
849
+ if (!allowedFor(principalKeysOfToken(model, canonical)))
850
+ continue;
515
851
  const record = model.roles.byCanonical.get(canonical);
516
852
  addPrincipal(record ? record.displayName : canonical, record?.kind ?? 'role');
517
853
  }
854
+ // The Admin write floor (see `hasPermissionResolved`): Admin holds write here
855
+ // whenever no nearer scope excludes it, whether or not any file names it.
856
+ if (verb === 'write' && model.roles.byCanonical.has(ADMIN_ROLE_KEY)) {
857
+ const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY);
858
+ if (allowedFor(principalKeysOfToken(model, ADMIN_ROLE_KEY)))
859
+ addPrincipal(adminRole.displayName, 'role');
860
+ }
861
+ // The derived everyone verdict: a public plugin principal granted here
862
+ // means anyone signed in holds the verb, and a list that counts holders (an
863
+ // approval gate, a "restricted to" banner) must say so — as `everyone`, the
864
+ // same row a literal grant produces.
865
+ if (everyone === 'grant' && allowedFor(NO_KEYS))
866
+ addPrincipal(EVERYONE_CANONICAL, 'role');
518
867
  // Mirror the admin overrides applied in `hasPermissionResolved`: write on
519
868
  // `roles.yaml` and on any `access.md` is granted to Admin even if the
520
869
  // file content doesn't list it. Without surfacing that here, the
@@ -525,7 +874,7 @@ function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
525
874
  // Look the Admin ROLE up via its explicit alias — the bare key may be
526
875
  // owned by a same-named group under group-first precedence. The override
527
876
  // is the ROLE's capability, so the row's kind is 'role' regardless.
528
- const adminRole = model.roles.byCanonical.get(`${ROLE_TOKEN_PREFIX}${ADMIN_CANONICAL}`);
877
+ const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY);
529
878
  addPrincipal(adminRole ? adminRole.displayName : ADMIN_CANONICAL, 'role');
530
879
  }
531
880
  const principals = [...byIdentity.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0);
@@ -537,11 +886,26 @@ function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
537
886
  for (const [email, state] of byEmail) {
538
887
  if (state !== 'grant')
539
888
  continue;
889
+ if (!hasPermissionResolved(model, verb, email, relativePath, fileOwn))
890
+ continue;
540
891
  users.push({ name: '', email });
541
892
  }
542
893
  users.sort((a, b) => a.email.localeCompare(b.email));
543
894
  return { principals, roles, users };
544
895
  }
896
+ /**
897
+ * Every individual the model names: `roles.yaml` members, emails named inline
898
+ * at this path's scope, and the deployment owner (Admin for the write floor
899
+ * even when `roles.yaml` does not list them).
900
+ */
901
+ function namedCandidateEmails(model, byEmail) {
902
+ const candidates = new Set(model.deploymentOwners);
903
+ for (const email of model.roles.byEmail.keys())
904
+ candidates.add(email);
905
+ for (const email of byEmail.keys())
906
+ candidates.add(email);
907
+ return candidates;
908
+ }
545
909
  /**
546
910
  * Expand the eligible-writer set to the underlying emails.
547
911
  *
@@ -561,12 +925,10 @@ function eligibleHolderEmailsResolved(model, verb, relativePath, fileOwn) {
561
925
  // in an access.md entry at this path's scope (whether granted or denied —
562
926
  // `hasPermissionResolved` filters denials out below). The built-in
563
927
  // `everyone` role can grant arbitrary signed-in users, but this finite
564
- // expansion can only return emails the access tree explicitly names.
565
- const candidates = new Set();
566
- for (const email of model.roles.byEmail.keys())
567
- candidates.add(email);
568
- for (const email of byEmail.keys())
569
- candidates.add(email);
928
+ // expansion can only return emails the access tree explicitly names. The
929
+ // deployment owner is a candidate too: the Admin write floor admits them
930
+ // whether or not `roles.yaml` lists them.
931
+ const candidates = namedCandidateEmails(model, byEmail);
570
932
  for (const email of candidates) {
571
933
  if (hasPermissionResolved(model, verb, email, relativePath, fileOwn)) {
572
934
  out.set(email, { name: '', email });
@@ -586,11 +948,7 @@ function eligibleHolderEmailsResolved(model, verb, relativePath, fileOwn) {
586
948
  */
587
949
  function ineligibleNamedEmailsResolved(model, verb, relativePath, fileOwn) {
588
950
  const { byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
589
- const candidates = new Set();
590
- for (const email of model.roles.byEmail.keys())
591
- candidates.add(email);
592
- for (const email of byEmail.keys())
593
- candidates.add(email);
951
+ const candidates = namedCandidateEmails(model, byEmail);
594
952
  const out = new Set();
595
953
  for (const email of candidates) {
596
954
  if (!hasPermissionResolved(model, verb, email, relativePath, fileOwn))
@@ -601,6 +959,8 @@ function ineligibleNamedEmailsResolved(model, verb, relativePath, fileOwn) {
601
959
  export class AccessControlService {
602
960
  workspaceService;
603
961
  kbDirName;
962
+ disk;
963
+ gitRunner;
604
964
  /**
605
965
  * Per-workspace cache: `model` is the resolved access tree and `loadedAt`
606
966
  * is when we computed it. We re-load on a TTL — the validator runs on
@@ -653,16 +1013,25 @@ export class AccessControlService {
653
1013
  * environment, not from the knowledge base.
654
1014
  */
655
1015
  deploymentOwners;
656
- constructor(workspaceService, kbDirName,
1016
+ constructor(workspaceService, kbDirName, disk,
657
1017
  /**
658
1018
  * Emails that count as Admin for the two hardcoded `write` rescues,
659
1019
  * whatever `roles.yaml` says — in practice `ADMIN_EMAIL`. Optional so the
660
1020
  * many test fixtures that construct this service directly keep their
661
1021
  * current behaviour (no owner, roles.yaml is the only authority).
662
1022
  */
663
- deploymentOwners = []) {
1023
+ deploymentOwners = [],
1024
+ /**
1025
+ * How git is run — see `shared/git.contract.ts`. The composition root
1026
+ * passes the one runner; the default is the same runner on its default
1027
+ * deadline, kept for the same reason as `deploymentOwners` above: a great
1028
+ * many fixtures construct this service directly.
1029
+ */
1030
+ gitRunner = new NodeGitRunner()) {
664
1031
  this.workspaceService = workspaceService;
665
1032
  this.kbDirName = kbDirName;
1033
+ this.disk = disk;
1034
+ this.gitRunner = gitRunner;
666
1035
  this.deploymentOwners = new Set(deploymentOwners.filter(Boolean).map((e) => canonicalEmail(e)));
667
1036
  }
668
1037
  /**
@@ -674,7 +1043,29 @@ export class AccessControlService {
674
1043
  invalidate(workspaceId) {
675
1044
  this.cache.delete(workspaceId);
676
1045
  this.ownEntriesCache.delete(workspaceId);
1046
+ this.generation++;
677
1047
  }
1048
+ /**
1049
+ * Bumped by every `invalidate()`, for any workspace. A model load reads the
1050
+ * tree across many awaits, and an invalidation fires exactly when that tree
1051
+ * changes underneath it: the load that started on the old tree would
1052
+ * otherwise store its model AFTER the drop, and every read for the next TTL
1053
+ * would resolve against a tree that is already gone. `loadModel` takes the
1054
+ * generation before its first read and refuses to store a model fetched
1055
+ * under an older one — the caller that started it still gets its answer,
1056
+ * as old as the moment it started, and the next caller reloads.
1057
+ *
1058
+ * ONE counter, not one per workspace: a per-workspace map would keep an
1059
+ * entry for every branch workspace the process ever loaded, with nothing to
1060
+ * tell it when a workspace is gone. The price of sharing is that a drop on
1061
+ * some other workspace, landing mid-load, costs this one a single reload —
1062
+ * a load is tens of milliseconds and a drop is a commit, so that is rare
1063
+ * and cheap, and it never stores a wrong model. (The own-entries memo needs
1064
+ * no token: it is keyed by the map object a reader took BEFORE its disk
1065
+ * read, and `invalidate()` replaces the map, so a late store lands in an
1066
+ * orphan nobody consults.)
1067
+ */
1068
+ generation = 0;
678
1069
  /** Memoized `readOwnEntries` for the batch paths; see `ownEntriesCache`. */
679
1070
  async cachedOwnEntries(workspaceId, repoDir, relativePath) {
680
1071
  let ws = this.ownEntriesCache.get(workspaceId);
@@ -727,6 +1118,35 @@ export class AccessControlService {
727
1118
  });
728
1119
  return result;
729
1120
  }
1121
+ async canReadAsGroupBatch(workspaceId, group, relativePaths) {
1122
+ const model = await this.loadModel(workspaceId);
1123
+ const keys = principalKeysOfGroup(model, group);
1124
+ if (keys === null)
1125
+ return null;
1126
+ const repoDir = await this.repoDir(workspaceId);
1127
+ // The same per-path own-entries `canReadBatch` folds in: a file's own
1128
+ // frontmatter rules are part of the walk for a group as for a person.
1129
+ const owns = await Promise.all(relativePaths.map((p) => this.cachedOwnEntries(workspaceId, repoDir, p)));
1130
+ const result = new Map();
1131
+ relativePaths.forEach((p, i) => {
1132
+ result.set(p, hasPermissionForKeys(model, 'read', keys, p, owns[i]));
1133
+ });
1134
+ return result;
1135
+ }
1136
+ async canReadAsEveryoneBatch(workspaceId, relativePaths) {
1137
+ const model = await this.loadModel(workspaceId);
1138
+ const repoDir = await this.repoDir(workspaceId);
1139
+ const owns = await Promise.all(relativePaths.map((p) => this.cachedOwnEntries(workspaceId, repoDir, p)));
1140
+ const result = new Map();
1141
+ relativePaths.forEach((p, i) => {
1142
+ // No keys at all: the walk then falls through to each scope's derived
1143
+ // `everyone` verdict — the built-in entry, or a public plugin's grant
1144
+ // or denial, whichever the scope builder settled on — which is exactly
1145
+ // what a caller holding nothing of their own resolves to.
1146
+ result.set(p, hasPermissionForKeys(model, 'read', NO_KEYS, p, owns[i]));
1147
+ });
1148
+ return result;
1149
+ }
730
1150
  async canWriteBatch(workspaceId, userEmail, relativePaths) {
731
1151
  const model = await this.loadModel(workspaceId);
732
1152
  const repoDir = await this.repoDir(workspaceId);
@@ -773,20 +1193,71 @@ export class AccessControlService {
773
1193
  async eligibleReaders(workspaceId, relativePath) {
774
1194
  const model = await this.loadModel(workspaceId);
775
1195
  const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
776
- // When `read: everyone` applies cleanly, the node is readable by all users
777
- // and the role/user lists are meaningless. Otherwise return the explicit
778
- // reader set; it may be empty for a default-denied path with no grants.
779
- if (canEveryoneReadResolved(model, relativePath, own)) {
780
- return { restricted: false, principals: [], roles: [], users: [] };
781
- }
1196
+ // `restricted` is the verdict — whether `read: everyone` applies cleanly —
1197
+ // and the lists are the GRANTS that exist regardless: on a public node
1198
+ // they name what makes it public (`everyone`, and a public plugin
1199
+ // principal whose grant is the one to remove), so the share dialog can
1200
+ // show that grant as a row. Consumers that only want to know whether the
1201
+ // node is public read the flag; the lists may be empty for a
1202
+ // default-denied path with no grants.
782
1203
  const { principals, roles, users } = eligibleHoldersResolved(model, 'read', relativePath, own);
783
- return { restricted: true, principals, roles, users };
1204
+ // WHY it is public, when it is: the public plugin principals granted read
1205
+ // here — the one thing the dialog cannot tell from the lists alone (a
1206
+ // plugin principal in them may or may not be public), so the resolver,
1207
+ // which knows, says it. A literal `everyone` line shows up as a source
1208
+ // of the `everyone` row instead; together the two answer "what remains
1209
+ // public after this grant is removed".
1210
+ const collapsed = resolveAtPath(model, 'read', relativePath, own);
1211
+ const publicVia = [...collapsed.byRole]
1212
+ .filter(([key, state]) => state === 'grant' && model.roles.publicKeys?.has(key))
1213
+ .map(([key]) => key)
1214
+ .sort();
1215
+ return { restricted: !canEveryoneReadResolved(model, relativePath, own), principals, roles, users, publicVia };
784
1216
  }
785
1217
  async eligibleDownloaders(workspaceId, relativePath) {
786
1218
  const model = await this.loadModel(workspaceId);
787
1219
  const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
788
1220
  return eligibleHoldersResolved(model, 'download', relativePath, own);
789
1221
  }
1222
+ /**
1223
+ * The read and write holders of one file at its current path and at a path
1224
+ * it has not moved to yet (see the interface).
1225
+ *
1226
+ * A move changes nothing about the file's own frontmatter — that travels
1227
+ * with the bytes — and everything about the folder chain above it. So the
1228
+ * hypothetical side is the ordinary resolution with the destination path
1229
+ * substituted: the same model, the same own-entries (read from the SOURCE,
1230
+ * since nothing sits at the destination to read), resolved against the
1231
+ * destination's ancestors. One model load and one file read serve all four
1232
+ * lookups.
1233
+ */
1234
+ async prospectiveHolders(workspaceId, fromPath, toPath) {
1235
+ const model = await this.loadModel(workspaceId);
1236
+ const repoDir = await this.repoDir(workspaceId);
1237
+ // A FILE question only. A folder carries its own `access.md` — which moves
1238
+ // with it and governs everything under it — so resolving it as a file
1239
+ // would read frontmatter it does not have and omit the rules it does,
1240
+ // naming principals that are not the ones a folder move changes. Refuse
1241
+ // rather than answer the wrong question convincingly.
1242
+ //
1243
+ // Only absence is an answer here (see `isAbsence`): a probe that failed on
1244
+ // permissions or I/O does not say "this is a file", and folding it into
1245
+ // one would let the very case above through on an unreadable source.
1246
+ const fromStat = await fs.stat(path.join(repoDir, fromPath)).catch((err) => {
1247
+ if (isAbsence(err))
1248
+ return null;
1249
+ throw err;
1250
+ });
1251
+ if (fromStat?.isDirectory()) {
1252
+ throw new WorkflowDomainError('prospective access answers for a file, not a folder', 400);
1253
+ }
1254
+ const own = await this.readOwnEntries(repoDir, fromPath);
1255
+ const holdersAt = (relativePath) => ({
1256
+ read: eligibleHoldersResolved(model, 'read', relativePath, own),
1257
+ write: eligibleHoldersResolved(model, 'write', relativePath, own),
1258
+ });
1259
+ return { before: holdersAt(fromPath), after: holdersAt(toPath) };
1260
+ }
790
1261
  async eligibleWriterEmails(workspaceId, relativePath) {
791
1262
  const model = await this.loadModel(workspaceId);
792
1263
  const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
@@ -813,13 +1284,92 @@ export class AccessControlService {
813
1284
  }
814
1285
  return out;
815
1286
  }
1287
+ async denialSources(workspaceId, kind, relativePath, principal, opts) {
1288
+ const model = await this.loadModel(workspaceId);
1289
+ // Same own-scope rule as `grantSources`: a file target consults its own
1290
+ // frontmatter, a folder target's own access.md is already in the dir chain.
1291
+ const own = kind === 'file'
1292
+ ? await this.readOwnEntries(await this.repoDir(workspaceId), relativePath)
1293
+ : null;
1294
+ const out = {};
1295
+ for (const verb of KNOWN_VERBS) {
1296
+ const sources = resolveDenialSourcesForVerb(model, verb, kind, relativePath, principal, own, opts?.tokenMatch);
1297
+ if (sources.length > 0)
1298
+ out[verb] = sources;
1299
+ }
1300
+ return out;
1301
+ }
1302
+ async locallyDeniedPrincipals(workspaceId, kind, relativePath) {
1303
+ const model = await this.loadModel(workspaceId);
1304
+ // The target's OWN entries, and only those — the one scope whose rules a
1305
+ // restriction made here is written into. A folder's are its `access.md` in
1306
+ // the dir model; a file node's are its own frontmatter.
1307
+ const own = kind === 'folder'
1308
+ ? (model.accessFilesByDir.get(relativePath)?.entries ?? null)
1309
+ : await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
1310
+ if (!own)
1311
+ return { principals: [], users: [] };
1312
+ const byIdentity = new Map();
1313
+ const byEmail = new Map();
1314
+ for (const verb of KNOWN_VERBS) {
1315
+ for (const entry of own[verb] ?? []) {
1316
+ // Grants are already reported by the eligible lists; only the denials
1317
+ // are invisible there, and only they are this method's business.
1318
+ if (!entry.deny)
1319
+ continue;
1320
+ if (entry.kind === 'user') {
1321
+ if (!byEmail.has(entry.email)) {
1322
+ byEmail.set(entry.email, { name: entry.displayName, email: entry.email });
1323
+ }
1324
+ continue;
1325
+ }
1326
+ // A token naming a role nobody knows is not a rule: `loadModel` drops
1327
+ // such entries from every `access.md`, and `resolveScopes` filters them
1328
+ // out of a file's own frontmatter (the one scope it does not pre-filter)
1329
+ // — so the resolver denies nothing, `denialSources` reports nothing, and
1330
+ // a row here would claim a restriction the server does not enforce. Skip
1331
+ // it on the same rule, so this list and the denial sources agree.
1332
+ if (!roleKnown(model.roles, entry.role))
1333
+ continue;
1334
+ // Resolve the token's kind and display name through the SAME merged
1335
+ // index the eligible lists use, so a denied group keys as `g:` and a
1336
+ // denied role as `r:` — matching the row keys the view is built on. A
1337
+ // KNOWN token with no record is a built-in (`everyone`, `admin` before
1338
+ // roles.yaml names it), which displays as the role it is.
1339
+ const record = model.roles.byCanonical.get(entry.role);
1340
+ const name = record ? record.displayName : (entry.displayRole || entry.role);
1341
+ const kindOf = record?.kind ?? 'role';
1342
+ const key = `${kindOf}\0${name.toLowerCase()}`;
1343
+ if (!byIdentity.has(key))
1344
+ byIdentity.set(key, { name, kind: kindOf });
1345
+ }
1346
+ }
1347
+ const principals = [...byIdentity.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0);
1348
+ const users = [...byEmail.values()].sort((a, b) => a.email.localeCompare(b.email));
1349
+ return { principals, users };
1350
+ }
1351
+ async explainAccess(workspaceId, userEmail, kind, relativePath) {
1352
+ const model = await this.loadModel(workspaceId);
1353
+ // The same own-entries read `canRead`/`canWrite`/… make, whatever the
1354
+ // kind: a directory simply has none.
1355
+ const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
1356
+ const out = {};
1357
+ for (const verb of KNOWN_VERBS) {
1358
+ const machineOwned = verb === 'write' ? this.machineOwnedWriteRule(userEmail, relativePath) : null;
1359
+ out[verb] =
1360
+ machineOwned !== null
1361
+ ? { allowed: machineOwned, source: null, via: 'machine-owned', principal: null }
1362
+ : explainDecision(model, decidePermissionResolved(model, verb, userEmail, relativePath, own), kind, relativePath);
1363
+ }
1364
+ return out;
1365
+ }
816
1366
  async kbPrincipals(workspaceId) {
817
1367
  let model;
818
1368
  try {
819
1369
  model = await this.loadModel(workspaceId);
820
1370
  }
821
1371
  catch {
822
- return { roles: [], groups: [], people: [] };
1372
+ return { roles: [], groups: [], plugins: [], people: [] };
823
1373
  }
824
1374
  // Roles = the built-in `everyone` role plus every declared role's display
825
1375
  // name — ROLE principals only, never groups. Each role is enumerated via
@@ -831,11 +1381,22 @@ export class AccessControlService {
831
1381
  // direct-access.md edit).
832
1382
  const roles = [EVERYONE_DISPLAY];
833
1383
  const groups = [];
1384
+ // Plugins = the identities behind the synthesised `plugin/<name>/<verb>`
1385
+ // principals, once each (three keys share a folder). Personal shelves are
1386
+ // plugins to the resolver but not to a person picking a grantee — told
1387
+ // apart by their FOLDER (the one structural rule), never by their name.
1388
+ const plugins = new Map();
834
1389
  for (const [key, principal] of model.roles.byCanonical) {
835
1390
  if (key.startsWith(ROLE_TOKEN_PREFIX))
836
1391
  roles.push(principal.displayName);
837
1392
  else if (principal.kind === 'group')
838
1393
  groups.push(principal.displayName);
1394
+ else if (principal.kind === 'plugin' &&
1395
+ principal.pluginName &&
1396
+ principal.pluginDir &&
1397
+ !isPersonalPluginDir(principal.pluginDir)) {
1398
+ plugins.set(principal.pluginName, principal.pluginDir);
1399
+ }
839
1400
  }
840
1401
  // People = roles.yaml member emails (name-less) ∪ access.md `Name <email>`
841
1402
  // grants (named). The login-only users table is unioned in by the caller.
@@ -857,7 +1418,14 @@ export class AccessControlService {
857
1418
  name: name || email.split('@')[0],
858
1419
  email,
859
1420
  }));
860
- return { roles, groups, people };
1421
+ return {
1422
+ roles,
1423
+ groups,
1424
+ plugins: [...plugins.entries()]
1425
+ .map(([name, folder]) => ({ name, folder }))
1426
+ .sort((a, b) => a.name.localeCompare(b.name)),
1427
+ people,
1428
+ };
861
1429
  }
862
1430
  async findEmailByHash(workspaceId, hash) {
863
1431
  let model;
@@ -887,7 +1455,7 @@ export class AccessControlService {
887
1455
  ...namesByEmail.keys(),
888
1456
  ]);
889
1457
  for (const email of candidates) {
890
- if (sha256Email(email) === hash) {
1458
+ if (hashEmail(email) === hash) {
891
1459
  const displayName = namesByEmail.get(email) ?? email.split('@')[0];
892
1460
  return { email, displayName };
893
1461
  }
@@ -906,7 +1474,7 @@ export class AccessControlService {
906
1474
  machineOwnedWriteRule(userEmail, relativePath) {
907
1475
  if (relativePath !== SYNCED_GROUPS_YAML)
908
1476
  return null;
909
- return userEmail.trim().toLowerCase() === DIRECTORY_SYNC_BOT_EMAIL;
1477
+ return canonicalEmail(userEmail) === DIRECTORY_SYNC_BOT_EMAIL;
910
1478
  }
911
1479
  async canWriteAtRef(workspaceId, ref, userEmail, relativePath) {
912
1480
  const machineOwned = this.machineOwnedWriteRule(userEmail, relativePath);
@@ -967,6 +1535,50 @@ export class AccessControlService {
967
1535
  const own = await this.readOwnEntriesAtRef(repoDir, loaded.resolvedRef, relativePath);
968
1536
  return eligibleHoldersResolved(loaded.model, 'write', relativePath, own);
969
1537
  }
1538
+ async holdsAdminRootWrite(workspaceId, userEmail) {
1539
+ const model = await this.loadModel(workspaceId);
1540
+ return isAdminEmail(model, canonicalEmail(userEmail));
1541
+ }
1542
+ async canRestorePlatformFile(workspaceId, userEmail, destinationRelativePath) {
1543
+ const target = platformRestoreDestination(destinationRelativePath);
1544
+ if (target === null)
1545
+ return false;
1546
+ const email = canonicalEmail(userEmail);
1547
+ let admin;
1548
+ try {
1549
+ admin = isAdminEmail(await this.loadModel(workspaceId), email);
1550
+ }
1551
+ catch {
1552
+ // The repository this rescue exists for is exactly the one whose
1553
+ // `roles.yaml` may be the file that went missing, and `loadModel`
1554
+ // throws when it cannot be read. With no model to ask, the deployment
1555
+ // owner is the only admin left — and they are the person who sets
1556
+ // `ADMIN_EMAIL`, so admitting them concedes nothing they did not have.
1557
+ admin = this.deploymentOwners.has(email);
1558
+ }
1559
+ if (!admin)
1560
+ return false;
1561
+ // A restore puts back what is MISSING, so the place it lands must be
1562
+ // empty. An `access.md` goes back only into a folder that has none —
1563
+ // into a folder that already has one it would not be a restore but a rule
1564
+ // change wearing a move's clothes, and the destination's own rules would
1565
+ // be the thing it bypassed the write gate to overwrite. The same holds at
1566
+ // the root: a move is a rename on disk, so landing `.bevelignore` on a
1567
+ // root that already has one would silently replace it, and the repository
1568
+ // was never missing that file to begin with.
1569
+ const destination = target.kind === 'root' ? target.name : path.join(target.dir, 'access.md');
1570
+ const repoDir = await this.repoDir(workspaceId);
1571
+ try {
1572
+ await fs.stat(path.join(repoDir, destination));
1573
+ return false;
1574
+ }
1575
+ catch (err) {
1576
+ if (isAbsence(err))
1577
+ return true;
1578
+ // Anything other than genuine absence is not an answer: fail closed.
1579
+ return false;
1580
+ }
1581
+ }
970
1582
  async eligibleWritersForPathsAtRef(workspaceId, ref, relativePaths) {
971
1583
  const result = new Map();
972
1584
  // Machine-owned paths resolve without the model — same answer
@@ -1012,6 +1624,8 @@ export class AccessControlService {
1012
1624
  if (cached && Date.now() - cached.loadedAt < AccessControlService.CACHE_TTL_MS) {
1013
1625
  return cached.model;
1014
1626
  }
1627
+ // Taken before the first read; see `generation`.
1628
+ const generation = this.generation;
1015
1629
  const repoDir = await this.repoDir(workspaceId);
1016
1630
  let rolesYaml;
1017
1631
  try {
@@ -1035,20 +1649,19 @@ export class AccessControlService {
1035
1649
  return await fs.readFile(path.join(repoDir, filename), 'utf-8');
1036
1650
  }
1037
1651
  catch (err) {
1038
- const code = err?.code;
1039
- if (code === 'ENOENT' || code === 'ENOTDIR')
1652
+ if (isAbsence(err))
1040
1653
  return null;
1041
1654
  throw err;
1042
1655
  }
1043
1656
  });
1044
1657
  if (!activeGroups.health.ok) {
1045
- console.error(`[access] groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`);
1658
+ log.error(`groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`);
1046
1659
  }
1047
1660
  for (const w of activeGroups.warnings)
1048
- console.warn(`[access] ${w}`);
1661
+ log.warn(w);
1049
1662
  const mergeWarnings = mergeGroupsIntoRoles(rolesParsed.index, activeGroups.groups, activeGroups.sourceFile);
1050
1663
  for (const w of mergeWarnings)
1051
- console.warn(`[access] ${w}`);
1664
+ log.warn(w);
1052
1665
  const accessFiles = new Map();
1053
1666
  // Walk the entire repo for `access.md` files. The access tree is
1054
1667
  // structure-agnostic — any `access.md` at any depth (including repo
@@ -1063,7 +1676,14 @@ export class AccessControlService {
1063
1676
  // must not 500 the entire editor; admins can still write `access.md`
1064
1677
  // / `roles.yaml` because `hasPermissionResolved` admin-rescues those
1065
1678
  // paths, so the bad config remains fixable from inside the app.
1066
- await this.collectAccessFiles(repoDir, '', accessFiles);
1679
+ const pluginDirs = new Map();
1680
+ await this.disk.walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
1681
+ // Plugin principals (`plugin/<Name>/<verb>`), derived from each plugin
1682
+ // folder's own access.md — a plugin being a folder with a manifest, at
1683
+ // any depth — after groups merged (so their rosters expand through the
1684
+ // finished index) and before the unknown-role sweep below (so a grant
1685
+ // naming one counts as known).
1686
+ synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginDirs);
1067
1687
  // Validate role refs against roles.yaml. `everyone` is a built-in role and
1068
1688
  // is valid without a roles.yaml entry. Unknown refs are dropped from the
1069
1689
  // parsed entry list (along with a warn log) so the rest of the file still
@@ -1072,7 +1692,7 @@ export class AccessControlService {
1072
1692
  for (const verb of KNOWN_VERBS) {
1073
1693
  file.entries[verb] = file.entries[verb].filter((entry) => {
1074
1694
  if (entry.kind === 'role' && !roleKnown(rolesParsed.index, entry.role)) {
1075
- console.warn(`[access] ${file.path}: '${verb}' references unknown role '${entry.displayRole}' — entry ignored`);
1695
+ log.warn(`${file.path}: '${verb}' references unknown role '${entry.displayRole}' — entry ignored`);
1076
1696
  return false;
1077
1697
  }
1078
1698
  return true;
@@ -1085,49 +1705,12 @@ export class AccessControlService {
1085
1705
  groupsHealth: activeGroups.health,
1086
1706
  deploymentOwners: this.deploymentOwners,
1087
1707
  };
1088
- this.cache.set(workspaceId, { model, loadedAt: Date.now() });
1089
- return model;
1090
- }
1091
- async collectAccessFiles(absDir, relDir, out) {
1092
- let entries;
1093
- try {
1094
- entries = await fs.readdir(absDir, { withFileTypes: true });
1095
- }
1096
- catch {
1097
- return;
1098
- }
1099
- for (const entry of entries) {
1100
- // Skip VCS metadata + vendored deps. Hidden dirs (`.git`, `.vscode`,
1101
- // etc.) and `node_modules` can't host KB rules and are often huge —
1102
- // walking them would slow every cache miss without benefit.
1103
- if (entry.name.startsWith('.'))
1104
- continue;
1105
- if (entry.name === 'node_modules')
1106
- continue;
1107
- const abs = path.join(absDir, entry.name);
1108
- const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
1109
- if (entry.isDirectory()) {
1110
- await this.collectAccessFiles(abs, rel, out);
1111
- }
1112
- else if (entry.isFile() && entry.name === 'access.md') {
1113
- const text = await fs.readFile(abs, 'utf-8');
1114
- const parsed = parseAccessFile(text, rel);
1115
- if (!parsed.ok) {
1116
- // Treat as if the file didn't exist for resolution purposes.
1117
- // Admin-rescue on access.md paths still lets an admin fix it.
1118
- for (const e of parsed.errors)
1119
- console.warn(`[access] ${e} — file ignored`);
1120
- continue;
1121
- }
1122
- for (const w of parsed.warnings)
1123
- console.warn(`[access] ${w}`);
1124
- if (out.has(parsed.file.dir)) {
1125
- console.warn(`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
1126
- continue;
1127
- }
1128
- out.set(parsed.file.dir, parsed.file);
1129
- }
1708
+ // An invalidation landed mid-load: this model describes a tree that is
1709
+ // already gone, so it is returned to this caller but never stored.
1710
+ if (this.generation === generation) {
1711
+ this.cache.set(workspaceId, { model, loadedAt: Date.now() });
1130
1712
  }
1713
+ return model;
1131
1714
  }
1132
1715
  async repoDir(workspaceId) {
1133
1716
  const wsDir = await this.workspaceService.getWorkspacePath(workspaceId);
@@ -1190,7 +1773,7 @@ export class AccessControlService {
1190
1773
  }
1191
1774
  if (wanted.length === 0)
1192
1775
  return result;
1193
- const texts = await catFileBatch(repoDir, wanted.map((p) => `${ref}:${p}`));
1776
+ const texts = await catFileBatch(this.gitRunner, repoDir, wanted.map((p) => `${ref}:${p}`));
1194
1777
  wanted.forEach((p, i) => {
1195
1778
  const text = texts[i];
1196
1779
  if (text === null)
@@ -1230,11 +1813,7 @@ export class AccessControlService {
1230
1813
  */
1231
1814
  async showAtRefOutcome(repoDir, ref, relativePath) {
1232
1815
  try {
1233
- const { stdout } = await execFileAsync('git', ['-C', repoDir, 'show', `${ref}:${relativePath}`], {
1234
- encoding: 'utf-8',
1235
- maxBuffer: 16 * 1024 * 1024,
1236
- env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
1237
- });
1816
+ const { stdout } = await this.gitRunner.run(repoDir, ['show', `${ref}:${relativePath}`]);
1238
1817
  return { kind: 'text', text: stdout };
1239
1818
  }
1240
1819
  catch (err) {
@@ -1253,16 +1832,20 @@ export class AccessControlService {
1253
1832
  */
1254
1833
  async listAccessFilesAtRef(repoDir, ref) {
1255
1834
  try {
1256
- const { stdout } = await execFileAsync('git', ['-C', repoDir, 'ls-tree', '-r', '--name-only', ref], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
1835
+ const { stdout } = await this.gitRunner.run(repoDir, ['ls-tree', '-r', '--name-only', ref]);
1257
1836
  const out = [];
1837
+ const pluginDirs = [];
1258
1838
  for (const line of stdout.split('\n')) {
1259
1839
  const p = line.trim();
1260
1840
  if (!p)
1261
1841
  continue;
1262
1842
  if (p === 'access.md' || p.endsWith('/access.md'))
1263
1843
  out.push(p);
1844
+ else if (p.startsWith(`${PLUGINS_DIR}/`) && p.endsWith(`/${PLUGIN_MANIFEST_FILE}`)) {
1845
+ pluginDirs.push(p.slice(0, -(PLUGIN_MANIFEST_FILE.length + 1)));
1846
+ }
1264
1847
  }
1265
- return out;
1848
+ return { accessFiles: out, pluginDirs };
1266
1849
  }
1267
1850
  catch {
1268
1851
  return 'error';
@@ -1338,11 +1921,21 @@ export class AccessControlService {
1338
1921
  if (!ref || ref.startsWith('-'))
1339
1922
  return null;
1340
1923
  try {
1341
- const { stdout } = await execFileAsync('git', ['-C', repoDir, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { encoding: 'utf-8' });
1924
+ const { stdout } = await this.gitRunner.run(repoDir, [
1925
+ 'rev-parse',
1926
+ '--verify',
1927
+ '--quiet',
1928
+ `${ref}^{commit}`,
1929
+ ]);
1342
1930
  const sha = stdout.trim();
1343
1931
  return /^[0-9a-f]{40,64}$/.test(sha) ? sha : null;
1344
1932
  }
1345
- catch {
1933
+ catch (err) {
1934
+ // Null means "does not resolve". A deadline says nothing about the ref,
1935
+ // and as a null it would turn a stalled git into a gate that quietly
1936
+ // reads no roles at that ref; it surfaces instead, as a read failure.
1937
+ if (isGitTimeout(err))
1938
+ throw err;
1346
1939
  return null;
1347
1940
  }
1348
1941
  }
@@ -1374,7 +1967,7 @@ export class AccessControlService {
1374
1967
  if (outcome.kind === 'error') {
1375
1968
  // The operational signal: one line per failed read, naming the commit
1376
1969
  // and the path, so a run of these is visible in the logs.
1377
- console.warn(`[access@${tag}] git read of ${relativePath} failed; refusing to decide from a partial tree`);
1970
+ logger(`access@${tag}`).warn(`git read of ${relativePath} failed; refusing to decide from a partial tree`);
1378
1971
  throw new AccessUnreadableError(label, relativePath);
1379
1972
  }
1380
1973
  return outcome.kind === 'text' ? outcome.text : null;
@@ -1394,20 +1987,20 @@ export class AccessControlService {
1394
1987
  // fires only on a file that was read and would not parse.
1395
1988
  const activeGroups = await loadActiveGroups(read);
1396
1989
  if (!activeGroups.health.ok) {
1397
- console.error(`[access@${tag}] groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`);
1990
+ logger(`access@${tag}`).error(`groups source ${activeGroups.health.file} is broken (${activeGroups.health.reason}) — groups contribute nothing until it is fixed`);
1398
1991
  }
1399
1992
  for (const w of activeGroups.warnings)
1400
- console.warn(`[access@${tag}] ${w}`);
1993
+ logger(`access@${tag}`).warn(w);
1401
1994
  const mergeWarnings = mergeGroupsIntoRoles(rolesParsed.index, activeGroups.groups, activeGroups.sourceFile);
1402
1995
  for (const w of mergeWarnings)
1403
- console.warn(`[access@${tag}] ${w}`);
1996
+ logger(`access@${tag}`).warn(w);
1404
1997
  const accessFiles = new Map();
1405
1998
  const listed = await this.listAccessFilesAtRef(repoDir, commit);
1406
1999
  if (listed === 'error') {
1407
- console.warn(`[access@${tag}] listing access.md files failed; refusing to decide from a partial tree`);
2000
+ logger(`access@${tag}`).warn('listing access.md files failed; refusing to decide from a partial tree');
1408
2001
  throw new AccessUnreadableError(label, 'access.md (ls-tree)');
1409
2002
  }
1410
- for (const p of listed) {
2003
+ for (const p of listed.accessFiles) {
1411
2004
  const text = await read(p);
1412
2005
  if (text === null)
1413
2006
  continue;
@@ -1419,6 +2012,16 @@ export class AccessControlService {
1419
2012
  // duplicates, but be defensive.
1420
2013
  accessFiles.set(parsed.file.dir, parsed.file);
1421
2014
  }
2015
+ // Mirror `loadModel`: plugin principals from the plugins the ref carries,
2016
+ // each under the identity its manifest AT THAT REF declares, so a PR-time
2017
+ // verdict on a skill granted to `plugin/<name>/write` counts the plugin's
2018
+ // writers exactly as the working tree would.
2019
+ const pluginIdentities = new Map();
2020
+ for (const dir of listed.pluginDirs) {
2021
+ const manifest = parseManifestText(await read(`${dir}/${PLUGIN_MANIFEST_FILE}`));
2022
+ pluginIdentities.set(dir, pluginIdentityOf(manifest, path.posix.basename(dir)));
2023
+ }
2024
+ synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginIdentities);
1422
2025
  // Mirror `loadModel`: drop entries whose role ref is unknown to roles.yaml,
1423
2026
  // while preserving the built-in `everyone` role.
1424
2027
  for (const [, file] of accessFiles) {