@bevel-software/platform-core-backend 0.33.0 → 0.35.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 (602) hide show
  1. package/agent-guide/access-control.md +146 -289
  2. package/agent-guide/conventions.md +23 -25
  3. package/agent-guide/directory-structure.md +72 -120
  4. package/agent-guide/drafts-and-change-requests.md +92 -0
  5. package/agent-guide/for-git-users.md +47 -0
  6. package/agent-guide/glossary.md +36 -0
  7. package/agent-guide/how-agents-find-things.md +49 -0
  8. package/agent-guide/html-views.md +239 -8
  9. package/agent-guide/new-knowledge-base.md +9 -8
  10. package/agent-guide/skills.md +67 -42
  11. package/agent-guide/start-here.md +65 -0
  12. package/agent-guide/tool-manuals.md +133 -155
  13. package/agent-guide/tools.md +67 -0
  14. package/agent-guide/where-a-new-file-goes.md +45 -28
  15. package/dist/core/catalog-revision.d.ts +12 -1
  16. package/dist/core/catalog-revision.d.ts.map +1 -1
  17. package/dist/core/catalog-revision.js +23 -3
  18. package/dist/core/catalog-revision.js.map +1 -1
  19. package/dist/core/core-ports.d.ts +41 -0
  20. package/dist/core/core-ports.d.ts.map +1 -1
  21. package/dist/core/core-ports.js.map +1 -1
  22. package/dist/core/create-core-server.d.ts.map +1 -1
  23. package/dist/core/create-core-server.js +15 -8
  24. package/dist/core/create-core-server.js.map +1 -1
  25. package/dist/core/create-core-services.d.ts +22 -5
  26. package/dist/core/create-core-services.d.ts.map +1 -1
  27. package/dist/core/create-core-services.js +44 -16
  28. package/dist/core/create-core-services.js.map +1 -1
  29. package/dist/core/lifecycle.d.ts +16 -0
  30. package/dist/core/lifecycle.d.ts.map +1 -1
  31. package/dist/core/lifecycle.js +12 -2
  32. package/dist/core/lifecycle.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/modules/access/access-control.interface.d.ts +39 -9
  37. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.d.ts +15 -3
  39. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.js +239 -75
  41. package/dist/modules/access/access-control.service.js.map +1 -1
  42. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  43. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  44. package/dist/modules/access/access-mutation.service.js +48 -51
  45. package/dist/modules/access/access-mutation.service.js.map +1 -1
  46. package/dist/modules/access/access-view.d.ts.map +1 -1
  47. package/dist/modules/access/access-view.js +12 -11
  48. package/dist/modules/access/access-view.js.map +1 -1
  49. package/dist/modules/access/access.routes.d.ts +3 -0
  50. package/dist/modules/access/access.routes.d.ts.map +1 -1
  51. package/dist/modules/access/access.routes.js +58 -33
  52. package/dist/modules/access/access.routes.js.map +1 -1
  53. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  54. package/dist/modules/access/groups-admin.routes.js +4 -2
  55. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  56. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  57. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  58. package/dist/modules/access/groups-admin.service.js +80 -38
  59. package/dist/modules/access/groups-admin.service.js.map +1 -1
  60. package/dist/modules/access/groups-edit.d.ts +1 -1
  61. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  62. package/dist/modules/access/groups-edit.js +1 -1
  63. package/dist/modules/access/groups-edit.js.map +1 -1
  64. package/dist/modules/access/index.d.ts +1 -1
  65. package/dist/modules/access/index.d.ts.map +1 -1
  66. package/dist/modules/access/reference-scan.d.ts +116 -24
  67. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  68. package/dist/modules/access/reference-scan.js +305 -44
  69. package/dist/modules/access/reference-scan.js.map +1 -1
  70. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  71. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  72. package/dist/modules/access/roles-admin.service.js +32 -23
  73. package/dist/modules/access/roles-admin.service.js.map +1 -1
  74. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  75. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  76. package/dist/modules/access-model/access-grammar.js +60 -20
  77. package/dist/modules/access-model/access-grammar.js.map +1 -1
  78. package/dist/modules/access-model/access-splice.d.ts +22 -18
  79. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  80. package/dist/modules/access-model/access-splice.js +20 -14
  81. package/dist/modules/access-model/access-splice.js.map +1 -1
  82. package/dist/modules/access-model/group-files.d.ts +1 -0
  83. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  84. package/dist/modules/access-model/group-files.js +5 -1
  85. package/dist/modules/access-model/group-files.js.map +1 -1
  86. package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
  87. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  88. package/dist/modules/agent-guide/agent-guide.js +103 -13
  89. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  90. package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
  91. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  92. package/dist/modules/agent-guide/agent-guide.tools.js +46 -22
  93. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  94. package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
  95. package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
  96. package/dist/modules/agent-guide/guide-budget.js +67 -0
  97. package/dist/modules/agent-guide/guide-budget.js.map +1 -0
  98. package/dist/modules/agent-guide/index.d.ts +2 -1
  99. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  100. package/dist/modules/agent-guide/index.js +2 -1
  101. package/dist/modules/agent-guide/index.js.map +1 -1
  102. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  103. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  104. package/dist/modules/agent-instructions/compose.js +7 -1
  105. package/dist/modules/agent-instructions/compose.js.map +1 -1
  106. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  107. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  108. package/dist/modules/agent-instructions/shared-file-rules.js +26 -9
  109. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  110. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  111. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  112. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  113. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  114. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  115. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  116. package/dist/modules/agent-scope/connection-scope.js +165 -0
  117. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  118. package/dist/modules/agent-scope/index.d.ts +3 -0
  119. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  120. package/dist/modules/agent-scope/index.js +3 -0
  121. package/dist/modules/agent-scope/index.js.map +1 -0
  122. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  123. package/dist/modules/audit/agent-audit.service.js +2 -0
  124. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  125. package/dist/modules/audit/audit.contract.d.ts +6 -0
  126. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  127. package/dist/modules/audit/audit.contract.js.map +1 -1
  128. package/dist/modules/auth/account-admission.d.ts +12 -0
  129. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  130. package/dist/modules/auth/account-admission.js +15 -0
  131. package/dist/modules/auth/account-admission.js.map +1 -1
  132. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  133. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  134. package/dist/modules/auth/auth.middleware.js +42 -4
  135. package/dist/modules/auth/auth.middleware.js.map +1 -1
  136. package/dist/modules/auth/auth.routes.d.ts +13 -9
  137. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  138. package/dist/modules/auth/auth.routes.js +35 -29
  139. package/dist/modules/auth/auth.routes.js.map +1 -1
  140. package/dist/modules/auth/auth.service.d.ts +55 -6
  141. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  142. package/dist/modules/auth/auth.service.js +128 -34
  143. package/dist/modules/auth/auth.service.js.map +1 -1
  144. package/dist/modules/auth/index.d.ts +1 -1
  145. package/dist/modules/auth/index.d.ts.map +1 -1
  146. package/dist/modules/auth/index.js +1 -1
  147. package/dist/modules/auth/index.js.map +1 -1
  148. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  149. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  150. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  151. package/dist/modules/code-mode/code-mode.tool.js +2 -2
  152. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  153. package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
  154. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  155. package/dist/modules/connection-probe/connection-probe.service.js +1 -38
  156. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  157. package/dist/modules/database/core-schema.d.ts +68 -0
  158. package/dist/modules/database/core-schema.d.ts.map +1 -1
  159. package/dist/modules/database/core-schema.js +33 -0
  160. package/dist/modules/database/core-schema.js.map +1 -1
  161. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  162. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  163. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  164. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  165. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  166. package/dist/modules/embed/embed.tools.js +5 -0
  167. package/dist/modules/embed/embed.tools.js.map +1 -1
  168. package/dist/modules/kb-fs/index.d.ts +2 -0
  169. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  170. package/dist/modules/kb-fs/index.js +1 -0
  171. package/dist/modules/kb-fs/index.js.map +1 -1
  172. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  173. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  174. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  175. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  176. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  177. package/dist/modules/kb-fs/repo-path.js +22 -7
  178. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  179. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  180. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  181. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  182. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  183. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  184. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  185. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  186. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  187. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  188. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  189. package/dist/modules/mcp/mcp.routes.js +39 -4
  190. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  191. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  192. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  193. package/dist/modules/mcp/mcp.service.js +74 -48
  194. package/dist/modules/mcp/mcp.service.js.map +1 -1
  195. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  196. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  197. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  198. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  199. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  200. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  201. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  202. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  203. package/dist/modules/plugins/index.d.ts +1 -1
  204. package/dist/modules/plugins/index.d.ts.map +1 -1
  205. package/dist/modules/plugins/index.js.map +1 -1
  206. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  207. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  208. package/dist/modules/plugins/plugin-links.js.map +1 -1
  209. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  210. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  211. package/dist/modules/plugins/plugins.routes.js +32 -5
  212. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  213. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
  214. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  215. package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
  216. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  217. package/dist/modules/secrets-vault/index.d.ts +1 -1
  218. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  219. package/dist/modules/secrets-vault/index.js +1 -1
  220. package/dist/modules/secrets-vault/index.js.map +1 -1
  221. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
  222. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  223. package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
  224. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  225. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  226. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  227. package/dist/modules/secrets-vault/secrets-vault.routes.js +100 -42
  228. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  229. package/dist/modules/settings/oidc-check.d.ts.map +1 -1
  230. package/dist/modules/settings/oidc-check.js +5 -17
  231. package/dist/modules/settings/oidc-check.js.map +1 -1
  232. package/dist/modules/skills/index.d.ts +3 -3
  233. package/dist/modules/skills/index.d.ts.map +1 -1
  234. package/dist/modules/skills/index.js +0 -2
  235. package/dist/modules/skills/index.js.map +1 -1
  236. package/dist/modules/skills/skills.contract.d.ts +16 -0
  237. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  238. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  239. package/dist/modules/skills/skills.service.js +6 -1
  240. package/dist/modules/skills/skills.service.js.map +1 -1
  241. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  242. package/dist/modules/skills/skills.tools.js +25 -13
  243. package/dist/modules/skills/skills.tools.js.map +1 -1
  244. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  245. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  246. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  247. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  248. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  249. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  250. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  251. package/dist/modules/tool-auth/index.d.ts +1 -1
  252. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  253. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  254. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  255. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  256. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  257. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  258. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  259. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  260. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  261. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  262. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  263. package/dist/modules/tool-helpers/tool-context.js +3 -0
  264. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  265. package/dist/modules/tool-helpers/tool-def.js +1 -1
  266. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  267. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  268. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  269. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  270. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  271. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  272. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  273. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  274. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  275. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  276. package/dist/modules/tool-helpers/validate-token.js +1 -1
  277. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  278. package/dist/modules/tool-manuals/index.d.ts +3 -3
  279. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  280. package/dist/modules/tool-manuals/index.js +0 -2
  281. package/dist/modules/tool-manuals/index.js.map +1 -1
  282. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
  283. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  284. package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
  285. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  286. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +46 -6
  287. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  288. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  289. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  290. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  291. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  292. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +24 -13
  293. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  294. package/dist/modules/tool-manuals/tool-manuals.service.js +138 -59
  295. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  296. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
  297. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  298. package/dist/modules/tool-manuals/tool-manuals.tools.js +56 -20
  299. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  300. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  301. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  302. package/dist/modules/tool-registry/manual.routes.js +27 -7
  303. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  304. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  305. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  306. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  307. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  308. package/dist/modules/workflow/agent-tools/workflow.tools.js +36 -27
  309. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  310. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  311. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  312. package/dist/modules/workflow/git/git.service.js +20 -35
  313. package/dist/modules/workflow/git/git.service.js.map +1 -1
  314. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  315. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  316. package/dist/modules/workflow/workflow.service.js +3 -0
  317. package/dist/modules/workflow/workflow.service.js.map +1 -1
  318. package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
  319. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
  320. package/dist/modules/workspace/agent-access.gate.js +1 -1
  321. package/dist/modules/workspace/agent-access.gate.js.map +1 -1
  322. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  323. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  324. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  325. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  326. package/dist/modules/workspace/index.d.ts +8 -0
  327. package/dist/modules/workspace/index.d.ts.map +1 -1
  328. package/dist/modules/workspace/index.js +8 -0
  329. package/dist/modules/workspace/index.js.map +1 -1
  330. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  331. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  332. package/dist/modules/workspace/nul-scan.js +55 -0
  333. package/dist/modules/workspace/nul-scan.js.map +1 -0
  334. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  335. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  336. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  337. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  338. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  339. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  340. package/dist/modules/workspace/ripgrep-search.js +657 -0
  341. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  342. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  343. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  344. package/dist/modules/workspace/ripgrep-version.js +31 -0
  345. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  346. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  347. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  348. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  349. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  350. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  351. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  352. package/dist/modules/workspace/worker-pool.js.map +1 -1
  353. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  354. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  355. package/dist/modules/workspace/workspace-tmp.js +435 -0
  356. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  357. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  358. package/dist/modules/workspace/workspace.routes.js +5 -26
  359. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  360. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  361. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  362. package/dist/modules/workspace/workspace.service.js +86 -6
  363. package/dist/modules/workspace/workspace.service.js.map +1 -1
  364. package/dist/modules/workspace/workspace.tools.d.ts +3 -2
  365. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  366. package/dist/modules/workspace/workspace.tools.js +531 -137
  367. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  368. package/dist/shared/oauth-token-error.d.ts +23 -0
  369. package/dist/shared/oauth-token-error.d.ts.map +1 -0
  370. package/dist/shared/oauth-token-error.js +47 -0
  371. package/dist/shared/oauth-token-error.js.map +1 -0
  372. package/dist/shared/path-order.d.ts +28 -8
  373. package/dist/shared/path-order.d.ts.map +1 -1
  374. package/dist/shared/path-order.js +37 -8
  375. package/dist/shared/path-order.js.map +1 -1
  376. package/dist/shared/read-capped.d.ts +19 -0
  377. package/dist/shared/read-capped.d.ts.map +1 -0
  378. package/dist/shared/read-capped.js +45 -0
  379. package/dist/shared/read-capped.js.map +1 -0
  380. package/migrations/0022_session_version.sql +6 -0
  381. package/migrations/0023_agent_path.sql +3 -0
  382. package/migrations/meta/0022_snapshot.json +2360 -0
  383. package/migrations/meta/0023_snapshot.json +2378 -0
  384. package/migrations/meta/_journal.json +14 -0
  385. package/package.json +4 -3
  386. package/src/__tests__/hexis-catalog.ts +144 -0
  387. package/src/__tests__/pg-scratch.test.ts +69 -0
  388. package/src/__tests__/pg-scratch.ts +31 -0
  389. package/src/__tests__/real-disk.ts +21 -0
  390. package/src/__tests__/slow-ripgrep.ts +65 -0
  391. package/src/__tests__/tokens.ts +19 -0
  392. package/src/__tests__/tool-manuals.ts +32 -0
  393. package/src/__tests__/tool-wiring.ts +25 -0
  394. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  395. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  396. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  397. package/src/core/__tests__/gated-boot.test.ts +8 -1
  398. package/src/core/__tests__/lifecycle.test.ts +46 -13
  399. package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
  400. package/src/core/catalog-revision.ts +24 -2
  401. package/src/core/core-ports.ts +42 -0
  402. package/src/core/create-core-server.ts +14 -1
  403. package/src/core/create-core-services.ts +62 -15
  404. package/src/core/lifecycle.ts +26 -2
  405. package/src/index.ts +1 -1
  406. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  407. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  408. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  409. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  410. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  411. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  412. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  413. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  414. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  415. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  416. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  417. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  418. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  419. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  420. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  421. package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -10
  422. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  423. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  424. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  425. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  426. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  427. package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
  428. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  429. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  430. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  431. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  432. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  433. package/src/modules/access/access-control.interface.ts +43 -10
  434. package/src/modules/access/access-control.service.ts +250 -72
  435. package/src/modules/access/access-mutation.service.ts +48 -53
  436. package/src/modules/access/access-view.ts +12 -10
  437. package/src/modules/access/access.routes.ts +66 -37
  438. package/src/modules/access/groups-admin.routes.ts +4 -2
  439. package/src/modules/access/groups-admin.service.ts +114 -44
  440. package/src/modules/access/groups-edit.ts +1 -1
  441. package/src/modules/access/index.ts +1 -1
  442. package/src/modules/access/reference-scan.ts +319 -46
  443. package/src/modules/access/roles-admin.service.ts +43 -26
  444. package/src/modules/access/synced-groups-writer.ts +1 -1
  445. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  446. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  447. package/src/modules/access-model/access-grammar.ts +65 -20
  448. package/src/modules/access-model/access-splice.ts +31 -23
  449. package/src/modules/access-model/group-files.ts +5 -0
  450. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +110 -25
  451. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
  452. package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
  453. package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
  454. package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
  455. package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
  456. package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
  457. package/src/modules/agent-guide/agent-guide.tools.ts +51 -22
  458. package/src/modules/agent-guide/agent-guide.ts +120 -13
  459. package/src/modules/agent-guide/guide-budget.ts +73 -0
  460. package/src/modules/agent-guide/index.ts +12 -0
  461. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +5 -4
  462. package/src/modules/agent-instructions/compose.ts +7 -1
  463. package/src/modules/agent-instructions/shared-file-rules.ts +27 -9
  464. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  465. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  466. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  467. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  468. package/src/modules/agent-scope/connection-scope.ts +199 -0
  469. package/src/modules/agent-scope/index.ts +15 -0
  470. package/src/modules/audit/agent-audit.service.ts +2 -0
  471. package/src/modules/audit/audit.contract.ts +6 -0
  472. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  473. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  474. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  475. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  476. package/src/modules/auth/account-admission.ts +19 -0
  477. package/src/modules/auth/auth.middleware.ts +45 -4
  478. package/src/modules/auth/auth.routes.ts +46 -29
  479. package/src/modules/auth/auth.service.ts +135 -34
  480. package/src/modules/auth/index.ts +2 -0
  481. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  482. package/src/modules/code-mode/code-mode.tool.ts +2 -2
  483. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
  484. package/src/modules/connection-probe/connection-probe.service.ts +1 -34
  485. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  486. package/src/modules/database/core-schema.ts +33 -0
  487. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  488. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  489. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  490. package/src/modules/embed/embed.tools.ts +5 -0
  491. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  492. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  493. package/src/modules/kb-fs/index.ts +2 -0
  494. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  495. package/src/modules/kb-fs/repo-path.ts +33 -7
  496. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  497. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  498. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  499. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  500. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  501. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  502. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  503. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  504. package/src/modules/mcp/mcp.routes.ts +39 -3
  505. package/src/modules/mcp/mcp.service.ts +112 -52
  506. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  507. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  508. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  509. package/src/modules/plugins/index.ts +1 -1
  510. package/src/modules/plugins/plugin-links.ts +19 -2
  511. package/src/modules/plugins/plugins.routes.ts +35 -4
  512. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  513. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
  514. package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
  515. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
  516. package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
  517. package/src/modules/secrets-vault/index.ts +1 -1
  518. package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
  519. package/src/modules/secrets-vault/secrets-vault.routes.ts +114 -42
  520. package/src/modules/settings/oidc-check.ts +5 -18
  521. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  522. package/src/modules/skills/index.ts +7 -2
  523. package/src/modules/skills/skills.contract.ts +17 -0
  524. package/src/modules/skills/skills.service.ts +5 -1
  525. package/src/modules/skills/skills.tools.ts +45 -14
  526. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  527. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  528. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  529. package/src/modules/tool-auth/index.ts +1 -1
  530. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  531. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  532. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
  533. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  534. package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
  535. package/src/modules/tool-helpers/tool-context.ts +10 -0
  536. package/src/modules/tool-helpers/tool-def.ts +1 -1
  537. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  538. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  539. package/src/modules/tool-helpers/validate-token.ts +4 -2
  540. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  541. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
  542. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  543. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +99 -8
  544. package/src/modules/tool-manuals/index.ts +6 -2
  545. package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
  546. package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -6
  547. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  548. package/src/modules/tool-manuals/tool-manuals.service.ts +169 -61
  549. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -25
  550. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
  551. package/src/modules/tool-registry/manual.routes.ts +26 -5
  552. package/src/modules/tool-registry/tool.contract.ts +7 -0
  553. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  554. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  555. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  556. package/src/modules/workflow/agent-tools/workflow.tools.ts +36 -27
  557. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  558. package/src/modules/workflow/git/git.service.ts +29 -48
  559. package/src/modules/workflow/workflow.service.ts +5 -0
  560. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  561. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  562. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  563. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  564. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  565. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  566. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  567. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  568. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -12
  569. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  570. package/src/modules/workspace/__tests__/workspace.tools.test.ts +549 -162
  571. package/src/modules/workspace/agent-access.gate.ts +1 -1
  572. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  573. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  574. package/src/modules/workspace/index.ts +20 -0
  575. package/src/modules/workspace/nul-scan.ts +50 -0
  576. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  577. package/src/modules/workspace/ripgrep-search.ts +715 -0
  578. package/src/modules/workspace/ripgrep-version.ts +35 -0
  579. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  580. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  581. package/src/modules/workspace/worker-pool.ts +3 -4
  582. package/src/modules/workspace/workspace-tmp.ts +457 -0
  583. package/src/modules/workspace/workspace.routes.ts +6 -27
  584. package/src/modules/workspace/workspace.service.ts +92 -15
  585. package/src/modules/workspace/workspace.tools.ts +574 -176
  586. package/src/shared/__tests__/read-capped.test.ts +68 -0
  587. package/src/shared/oauth-token-error.ts +46 -0
  588. package/src/shared/path-order.ts +37 -8
  589. package/src/shared/read-capped.ts +40 -0
  590. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  591. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  592. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  593. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  594. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  595. package/agent-guide/finding-things.md +0 -7
  596. package/agent-guide/introduction.md +0 -27
  597. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  598. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  599. package/dist/modules/workspace/bounded-matcher.js +0 -200
  600. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  601. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  602. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -132,6 +132,7 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
132
132
  workspace,
133
133
  workflow.svc,
134
134
  new AccessControlService(workspace as never, KB, new NodeFs()),
135
+ new NodeFs(),
135
136
  KB,
136
137
  () => DEFAULT_BRANCH,
137
138
  );
@@ -145,7 +146,7 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
145
146
  const groupsYaml = () => fs.readFile(path.join(repo, 'groups.yaml'), 'utf-8');
146
147
 
147
148
  it('roster: capability metadata, legacy flag, and members/groups split', async () => {
148
- const roster = await service.getRoster();
149
+ const roster = await service.getRoster(ADMIN.email);
149
150
  const admin = roster.find((r) => r.canonical === 'admin')!;
150
151
  expect(admin.capability).toMatchObject({ groupAssignable: true });
151
152
  expect(admin.capability?.description).toContain('configuration');
@@ -181,7 +182,7 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
181
182
  it('assigns a group to Admin (NEW) — the invariant lives in the direct-email rule, not a ban', async () => {
182
183
  await service.assignGroup(ADMIN, 'admin', 'GTM Team');
183
184
  expect(await rolesYaml()).toContain('- group:gtm team');
184
- const roster = await service.getRoster();
185
+ const roster = await service.getRoster(ADMIN.email);
185
186
  expect(roster.find((r) => r.canonical === 'admin')?.groups).toEqual(['gtm team']);
186
187
  // But Admin can never drop its LAST direct email — group refs don't count.
187
188
  await expect(
@@ -216,6 +217,7 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
216
217
  workspace,
217
218
  workflow.svc,
218
219
  new AccessControlService(workspace as never, KB, new NodeFs()),
220
+ new NodeFs(),
219
221
  KB,
220
222
  () => DEFAULT_BRANCH,
221
223
  );
@@ -279,6 +281,7 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
279
281
  workspace,
280
282
  workflow.svc,
281
283
  new AccessControlService(workspace as never, KB, new NodeFs()),
284
+ new NodeFs(),
282
285
  KB,
283
286
  () => DEFAULT_BRANCH,
284
287
  );
@@ -303,6 +306,7 @@ describe('RolesAdminService — capabilities, group assignment, conversion', ()
303
306
  workspace,
304
307
  workflow.svc,
305
308
  new AccessControlService(workspace as never, KB, new NodeFs()),
309
+ new NodeFs(),
306
310
  KB,
307
311
  () => DEFAULT_BRANCH,
308
312
  );
@@ -182,7 +182,7 @@ async function makeHarness(
182
182
  (req as unknown as { userId: string }).userId = ADMIN.id;
183
183
  next();
184
184
  });
185
- app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB })));
185
+ app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB }), new NodeFs()));
186
186
 
187
187
  const server = await new Promise<Server>((resolve) => {
188
188
  const s = app.listen(0, () => resolve(s));
@@ -55,8 +55,11 @@ export type GrantPrincipal =
55
55
  * One collective principal in a resolved eligible list, with WHAT it is: a
56
56
  * ROLE (an app-defined capability) or a GROUP (a grant audience from the
57
57
  * active group source). The resolver knows the kind from the merged principal
58
- * index — a `role/<canonical>` alias hit is always the role; a bare token is
59
- * whatever owns it under group-first precedence — and the share dialog needs
58
+ * index — a `role/<canonical>` alias hit is always the role, a
59
+ * `group/<canonical>` hit always the group, and a bare token is the role when
60
+ * one of that name exists, otherwise the group when one exists (a token that
61
+ * names neither keeps the legacy `role` kind, as `everyone` does) — and the
62
+ * share dialog needs
60
63
  * it to badge each grantee row honestly ("Role" vs "Group") and to round-trip
61
64
  * the row's principal with the right kind.
62
65
  *
@@ -89,6 +92,17 @@ export type PathHolders = { read: HolderList; write: HolderList };
89
92
  */
90
93
  export type ProspectiveHolders = { before: PathHolders; after: PathHolders };
91
94
 
95
+ /** One principal that loses or gains one verb in a move. A person is named by their address when they have no display name. */
96
+ export type HolderChange = {
97
+ kind: 'group' | 'role' | 'plugin' | 'person';
98
+ name: string;
99
+ email?: string;
100
+ verb: 'read' | 'write';
101
+ };
102
+
103
+ /** Who loses and who gains opening or editing a file or folder in a move — see {@link IAccessControl.prospectiveHolderChanges}. */
104
+ export type HolderChanges = { lose: HolderChange[]; gain: HolderChange[] };
105
+
92
106
  /** One caller's own verdict per access verb on one path. */
93
107
  export type PathVerbs = { read: boolean; write: boolean; download: boolean; owner: boolean };
94
108
 
@@ -417,6 +431,21 @@ export interface IAccessControl {
417
431
  toPath: string,
418
432
  ): Promise<ProspectiveHolders>;
419
433
 
434
+ /**
435
+ * Who loses and who gains `read` or `write` on one file or folder if a move
436
+ * puts it at `toPath`, over the same relocation as
437
+ * {@link IAccessControl.prospectiveHolders}. A holder on one side loses (or
438
+ * gains) the verb only when it does not hold it on the other side: the
439
+ * lists name grants, so a group no rule names at the destination may still
440
+ * read there through `everyone`, and one a rule denies there may not.
441
+ * Writes nothing and moves nothing.
442
+ */
443
+ prospectiveHolderChanges(
444
+ workspaceId: string,
445
+ fromPath: string,
446
+ toPath: string,
447
+ ): Promise<HolderChanges>;
448
+
420
449
  /**
421
450
  * The CALLER'S OWN `{ read, write, download, owner }` at `toPath` as they
422
451
  * will be once `fromPath` has been moved (`sourceRemains: false`, the
@@ -438,13 +467,19 @@ export interface IAccessControl {
438
467
  * tree that does. It writes nothing and moves nothing, and it answers the
439
468
  * caller's own verbs only — never the content of an `access.md` they may
440
469
  * not read.
470
+ *
471
+ * `sourceContent` previews a copy whose source is NO repository path — a
472
+ * file staged in the workspace's `tmp/` folder: its text, read exactly as a
473
+ * repository file's would be (frontmatter as the name it lands under, an
474
+ * `access.md` landing as that folder's rules). `fromPath` then only names
475
+ * the source; nothing is read at it.
441
476
  */
442
477
  previewAccessAfterRelocation(
443
478
  workspaceId: string,
444
479
  userEmail: string,
445
480
  fromPath: string,
446
481
  toPath: string,
447
- opts?: { sourceRemains?: boolean },
482
+ opts?: { sourceRemains?: boolean; sourceContent?: string },
448
483
  ): Promise<PathVerbs>;
449
484
 
450
485
  /**
@@ -490,13 +525,11 @@ export interface IAccessControl {
490
525
  * access is omitted. A principal with no access anywhere yields an empty map.
491
526
  *
492
527
  * `opts.tokenMatch: 'exact'` pins a ROLE-shaped principal to its literal
493
- * token spelling: only the exact token (bare, or `role/<name>`) counts as
494
- * the principal's own entry, regardless of group shadowing. The mutation
495
- * layer uses this so a check runs against the SAME identity a pinned
496
- * exact-token splice edited — e.g. verifying a GROUP deny whose group has
497
- * vanished, where the default (alias-tolerant when unshadowed) matching
498
- * would misattribute a same-named role's surviving `role/<name>` grant to
499
- * the group. Omitted (or `'name'`) → the shadowing-derived default.
528
+ * token spelling: only the exact token (bare, `role/<name>` or
529
+ * `group/<name>`) counts as the principal's own entry. The mutation layer
530
+ * uses this so a check runs against the SAME identity a pinned exact-token
531
+ * splice edited. Omitted (or `'name'`) → the explicit spelling plus the
532
+ * bare name when the bare name is this principal's (role-first).
500
533
  */
501
534
  grantSources(
502
535
  workspaceId: string,
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { StringDecoder } from 'node:string_decoder';
3
4
  import { logger } from '../../shared/logging.js';
4
5
 
5
6
  const log = logger('access');
@@ -21,30 +22,34 @@ import type {
21
22
  GrantSources,
22
23
  PathHolders,
23
24
  PathVerbs,
25
+ HolderChange,
26
+ HolderChanges,
24
27
  ProspectiveHolders,
25
28
  ResolvedPrincipal,
26
29
  } from './access-control.interface.js';
27
30
  import {
28
31
  DEFAULT_KB_LAYOUT,
29
32
  PLUGIN_MANIFEST_FILE,
33
+ isFrontmatterFence,
30
34
  isPersonalPluginDir,
31
35
  pluginIdentityOf,
32
36
  platformRestoreDestination,
33
37
  } from '@bevel-software/platform-shared';
34
38
  import type { KbContext } from '../../shared/kb-context.js';
35
- import { AccessConfigError, AccessUnreadableError, synthesizePluginPrincipals } from '../access-model/index.js';
36
39
  import {
40
+ AccessConfigError,
41
+ AccessUnreadableError,
42
+ synthesizePluginPrincipals,
37
43
  GROUPS_YAML,
38
44
  SYNCED_GROUPS_YAML,
39
45
  parseGroupsFile,
40
- type GroupsIndex,
41
- } from '../access-model/index.js';
42
- import {
43
46
  ADMIN_CANONICAL,
44
47
  EVERYONE_CANONICAL,
45
48
  EVERYONE_DISPLAY,
46
49
  KNOWN_VERBS,
47
50
  ROLE_TOKEN_PREFIX,
51
+ GROUP_TOKEN_PREFIX,
52
+ splitPrincipalToken,
48
53
  accessMdSelfEntries,
49
54
  canonicalEmail,
50
55
  canonicalRoleName,
@@ -55,6 +60,7 @@ import {
55
60
  parseRolesYaml,
56
61
  requiredVerbsFor,
57
62
  sourceVerbsFor,
63
+ type GroupsIndex,
58
64
  type AccessFile,
59
65
  type OwnEntries,
60
66
  type ParsedEntry,
@@ -567,17 +573,20 @@ function addDerivedKeys(model: AccessModel, keys: Set<string>): Set<string> {
567
573
  * twin of `principalKeysOf`: a member's key set minus everything they hold
568
574
  * for reasons of their own. `null` when no group of that name exists.
569
575
  *
570
- * Groups are keyed by their bare canonical name (`mergeGroupsIntoRoles`);
571
- * roles by `role/<name>`. A bare key whose record is a ROLE is not a group,
572
- * however it was spelled.
576
+ * Groups are keyed by `group/<name>`, and by their bare canonical name
577
+ * when no role owns it (`mergeGroupsIntoRoles`); roles by the bare name and
578
+ * `role/<name>`. `group` may be spelled either way: a bare name whose record
579
+ * is a ROLE still finds the group through its `group/` alias, because this
580
+ * question is always asked of the group.
573
581
  */
574
582
  function principalKeysOfGroup(model: AccessModel, group: string): Set<string> | null {
575
- const canonical = canonicalRoleName(group);
576
- const record = model.roles.byCanonical.get(canonical);
583
+ const { name } = splitPrincipalToken(canonicalRoleName(group));
584
+ const record = model.roles.byCanonical.get(`${GROUP_TOKEN_PREFIX}${name}`);
577
585
  if (!record || record.kind !== 'group') return null;
578
- const keys = new Set<string>([canonical]);
586
+ const keys = new Set<string>();
579
587
  for (const [key, principal] of model.roles.byCanonical) {
580
- if (principal.kind === 'role' && principal.groupRefs?.has(canonical)) keys.add(key);
588
+ if (principal === record) keys.add(key);
589
+ else if (principal.kind === 'role' && principal.groupRefs?.has(name)) keys.add(key);
581
590
  }
582
591
  // Plugin principals were expanded from role/group entries; a plugin that
583
592
  // admits the group, or a role the group is folded into, admits the group.
@@ -634,9 +643,9 @@ function isAdminFloorScope(scope: AccessScope, relativePath: string): boolean {
634
643
  function isAdminEmail(model: AccessModel, email: string): boolean {
635
644
  if (model.deploymentOwners.has(email)) return true;
636
645
  const roles = model.roles.byEmail.get(email);
637
- // Check the explicit `role/admin` alias, NOT the bare token: bare-name
638
- // precedence is group-first, so a group that happens to be named "Admin"
639
- // owns the bare key — and its members must never inherit the capability.
646
+ // Check the explicit `role/admin` alias: it is the role's whatever the
647
+ // bare key holds, and a group that happens to be named "Admin" is only
648
+ // ever `group/admin` — its members never inherit the capability.
640
649
  return !!roles && roles.has(ADMIN_ROLE_KEY);
641
650
  }
642
651
 
@@ -885,26 +894,11 @@ function resolveGrantSourcesForVerb(
885
894
 
886
895
  if (principal.kind === 'role') {
887
896
  // A named principal can be spelled two ways in a file: the bare token and
888
- // the explicit `role/<name>` token. WHICH spellings are THIS principal's
889
- // entries depends on who owns the bare key in the merged index:
890
- //
891
- // - UNSHADOWED (no group named `<bare>`): both spellings resolve to the
892
- // role, so both count — a grant under either adds a source; a deny
893
- // under either cuts off farther grants. (The revoke splice strips both
894
- // spellings in this case too, so classification and removal agree.)
895
- // - SHADOWED (a group owns the bare key): the spellings are DIFFERENT
896
- // principals. A bare-token principal is the GROUP — only bare entries
897
- // are its own; a `role/`-token principal is the ROLE — only `role/`
898
- // entries are its own. Counting the other spelling would attribute a
899
- // shadowed bare token to the role (hiding a real `role/<name>`
900
- // ancestor grant and making its revoke a false no-op), or vice versa.
901
- //
902
- // - PINNED EXACT (`tokenMatch: 'exact'`): only the literally-spelled
903
- // token is this principal's entry, shadowing notwithstanding. The
904
- // caller pinned the same identity into the splice it is checking —
905
- // a GROUP whose group has VANISHED reads "unshadowed" here, and the
906
- // alias-tolerant pair would then attribute a same-named role's
907
- // surviving `role/<name>` grant to the group.
897
+ // its explicit `role/<name>` or `group/<name>` token. Which spellings
898
+ // are THIS principal's entries is `ownTokensOf`'s one rule — the bare
899
+ // token counts only when the bare key is this principal's (the role
900
+ // when one exists, else the group), and a pinned `'exact'` counts the
901
+ // literal spelling alone.
908
902
  const tokens = ownTokensOf(model, principal.role, tokenMatch);
909
903
  for (const scope of scopes) {
910
904
  const states = tokens.map((t) => scope.byRole.get(t));
@@ -933,11 +927,21 @@ function resolveGrantSourcesForVerb(
933
927
 
934
928
  /**
935
929
  * WHICH token spellings in a file are THIS role-shaped principal's own entries
936
- * — the shadowing rule, in one place because the grant walk, the denial walk
937
- * and the revoke splice must all agree on it (a disagreement attributes a
938
- * shadowed bare token to the wrong principal, which is how a "restrict" silently
939
- * strips a same-named role's grant). See `resolveGrantSourcesForVerb` for the
940
- * full reasoning behind each branch.
930
+ * — in one place because the grant walk, the denial walk and the revoke splice
931
+ * must all agree on it (a disagreement attributes a bare token to the wrong
932
+ * principal, which is how a "restrict" silently strips a same-named role's
933
+ * grant).
934
+ *
935
+ * - `role/X` is the role: its own entries are `role/X`, plus the bare `X`
936
+ * unless a GROUP owns the bare key (no role X exists, a group X does).
937
+ * - `group/X` is the group: `group/X`, plus the bare `X` unless a ROLE
938
+ * owns it — under role-first precedence a same-named role always does.
939
+ * - A bare `X` is whoever owns the bare key, with that kind's explicit
940
+ * spelling; a bare token with no record (`everyone`, a plugin token, a
941
+ * vanished principal) is just itself plus the `role/` spelling a legacy
942
+ * role grant may carry.
943
+ * - PINNED EXACT (`tokenMatch: 'exact'`): only the literal spelling. The
944
+ * caller pinned the same identity into the splice it is checking.
941
945
  */
942
946
  function ownTokensOf(
943
947
  model: AccessModel,
@@ -945,12 +949,14 @@ function ownTokensOf(
945
949
  tokenMatch?: 'exact' | 'name',
946
950
  ): string[] {
947
951
  const canonical = canonicalRoleName(role);
948
- const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
949
- const bare = explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical;
950
- const shadowed = model.roles.byCanonical.get(bare)?.kind === 'group';
951
- return tokenMatch === 'exact' || shadowed
952
- ? [explicit ? `${ROLE_TOKEN_PREFIX}${bare}` : bare]
953
- : [bare, `${ROLE_TOKEN_PREFIX}${bare}`];
952
+ if (tokenMatch === 'exact') return [canonical];
953
+ const { prefix, name } = splitPrincipalToken(canonical);
954
+ const bareOwner = model.roles.byCanonical.get(name)?.kind;
955
+ if (prefix === 'role') return bareOwner === 'group' ? [canonical] : [name, canonical];
956
+ if (prefix === 'group') return bareOwner === 'role' ? [canonical] : [name, canonical];
957
+ if (bareOwner === 'group') return [name, `${GROUP_TOKEN_PREFIX}${name}`];
958
+ if (bareOwner === 'plugin') return [name];
959
+ return [name, `${ROLE_TOKEN_PREFIX}${name}`];
954
960
  }
955
961
 
956
962
  /**
@@ -1085,6 +1091,71 @@ function parentDirOf(relativePath: string): string {
1085
1091
  return cut === -1 ? '' : relativePath.slice(0, cut);
1086
1092
  }
1087
1093
 
1094
+ /** The first read of {@link frontmatterHead}; each later one doubles, up to the last. */
1095
+ const HEAD_FIRST_READ = 1_024;
1096
+ const HEAD_LAST_READ = 64 * 1_024;
1097
+
1098
+ /**
1099
+ * The start of `file` that holds everything its frontmatter can say: its
1100
+ * first line when that is no fence (no frontmatter at all), the text through
1101
+ * the closing fence when there is one, and the whole file when an opening
1102
+ * fence is never closed. Scanned for its frontmatter (`scanFrontmatter`), it
1103
+ * answers exactly as the whole file does, by the same fence rule
1104
+ * (`isFrontmatterFence`) — and a file with no frontmatter costs one small
1105
+ * read however long it is.
1106
+ */
1107
+ async function frontmatterHead(file: string): Promise<string> {
1108
+ const handle = await fs.open(file, 'r');
1109
+ try {
1110
+ const decoder = new StringDecoder('utf8');
1111
+ // What was read, kept as it was decoded and joined once, when answered:
1112
+ // each character is scanned once, however long a line runs.
1113
+ const pieces: string[] = [];
1114
+ let before = 0;
1115
+ // The line being read so far, folded (see `foldLine`), or null once it
1116
+ // can no longer be a fence.
1117
+ let line: string | null = '';
1118
+ let first = true;
1119
+ let position = 0;
1120
+ for (let size = HEAD_FIRST_READ; ; size = Math.min(size * 2, HEAD_LAST_READ)) {
1121
+ const chunk = Buffer.allocUnsafe(size);
1122
+ const { bytesRead } = await handle.read(chunk, 0, size, position);
1123
+ if (bytesRead === 0) return pieces.join('') + decoder.end();
1124
+ position += bytesRead;
1125
+ const piece = decoder.write(chunk.subarray(0, bytesRead));
1126
+ pieces.push(piece);
1127
+ let from = 0;
1128
+ for (let nl = piece.indexOf('\n'); nl !== -1; nl = piece.indexOf('\n', from)) {
1129
+ const fence = line !== null && isFrontmatterFence(foldLine(line, piece.slice(from, nl)));
1130
+ // The first line opens a block or there is none; after it, the first
1131
+ // fence closes the block.
1132
+ if (first ? !fence : fence) return pieces.join('').slice(0, before + nl);
1133
+ first = false;
1134
+ line = '';
1135
+ from = nl + 1;
1136
+ }
1137
+ if (line !== null) {
1138
+ line = foldLine(line, piece.slice(from));
1139
+ if (!'---'.startsWith(line.trim())) line = null;
1140
+ }
1141
+ // A first line still unfinished that already cannot be a fence.
1142
+ if (first && line === null) return pieces.join('');
1143
+ before += piece.length;
1144
+ }
1145
+ } finally {
1146
+ await handle.close();
1147
+ }
1148
+ }
1149
+
1150
+ /**
1151
+ * `line` continued by `more`, every run of whitespace folded to one space.
1152
+ * Trimmed, it is `---` exactly when the unfolded line is, so it judges a fence
1153
+ * as `isFrontmatterFence` does while staying a few characters long.
1154
+ */
1155
+ function foldLine(line: string, more: string): string {
1156
+ return `${line}${more}`.replace(/\s+/g, ' ');
1157
+ }
1158
+
1088
1159
  /**
1089
1160
  * One file's own-entries, parsed as the path `asName` would be read.
1090
1161
  *
@@ -1209,15 +1280,15 @@ function eligibleHoldersResolved(
1209
1280
 
1210
1281
  // (kind, display name) → one entry. The `byRole` keys are the merged
1211
1282
  // index's canonical tokens exactly as granted (bare, or the
1212
- // `role/<canonical>` alias); the `byCanonical` record a key hits carries
1213
- // its kind — a `role/` alias always hits the role record, a bare key hits
1214
- // whichever principal owns it under group-first precedence. A token with no
1215
- // record (the built-in `everyone`, or a grant naming a since-vanished
1216
- // principal) degrades to 'role', the pre-groups display. When one display
1217
- // name is granted as BOTH (a role via its `role/` alias plus a same-named
1218
- // group via the bare token) BOTH entries survive — they are DIFFERENT
1219
- // principals, and collapsing them to one would hide the role's live
1220
- // `role/<name>` grant from every consumer of the eligible list.
1283
+ // `role/` or `group/` alias); the `byCanonical` record a key hits carries
1284
+ // its kind — a `role/` alias always hits the role record, a `group/` alias
1285
+ // the group record, a bare key the role when one exists, else the group. A
1286
+ // token with no record (the built-in `everyone`, or a grant naming a
1287
+ // since-vanished principal) degrades to 'role', the pre-groups display.
1288
+ // When one display name is granted as BOTH (the role plus a same-named
1289
+ // group via `group/`) BOTH entries survive — they are DIFFERENT
1290
+ // principals, and collapsing them to one would hide one of the grants from
1291
+ // every consumer of the eligible list.
1221
1292
  const byIdentity = new Map<string, ResolvedPrincipal>();
1222
1293
  const addPrincipal = (name: string, kind: ResolvedPrincipal['kind']) => {
1223
1294
  const key = `${kind}\0${name.toLowerCase()}`;
@@ -1259,9 +1330,9 @@ function eligibleHoldersResolved(
1259
1330
  verb === 'write' &&
1260
1331
  (relativePath === 'roles.yaml' || isAccessMdPath(relativePath))
1261
1332
  ) {
1262
- // Look the Admin ROLE up via its explicit alias — the bare key may be
1263
- // owned by a same-named group under group-first precedence. The override
1264
- // is the ROLE's capability, so the row's kind is 'role' regardless.
1333
+ // Look the Admin ROLE up via its explicit alias, the key that is always
1334
+ // the role's. The override is the ROLE's capability, so the row's kind is
1335
+ // 'role' regardless.
1265
1336
  const adminRole = model.roles.byCanonical.get(ADMIN_ROLE_KEY);
1266
1337
  addPrincipal(adminRole ? adminRole.displayName : ADMIN_CANONICAL, 'role');
1267
1338
  }
@@ -1285,6 +1356,70 @@ function eligibleHoldersResolved(
1285
1356
  return { principals, roles, users };
1286
1357
  }
1287
1358
 
1359
+ /** One side of a move: the access model, the path, and the rules the file carries there. */
1360
+ type MoveSide = { model: AccessModel; path: string; own: OwnEntries | null };
1361
+
1362
+ /** The key set behind one entry of an eligible-holders list, read back from its name and kind. */
1363
+ function keysOfHolder(model: AccessModel, holder: ResolvedPrincipal): ReadonlySet<string> {
1364
+ for (const [canonical, record] of model.roles.byCanonical) {
1365
+ if (record.kind === holder.kind && record.displayName === holder.name) return principalKeysOfToken(model, canonical);
1366
+ }
1367
+ return principalKeysOfToken(model, canonicalRoleName(holder.name));
1368
+ }
1369
+
1370
+ /**
1371
+ * Who loses and who gains `verb` between the two sides of a move. The
1372
+ * candidates are every principal and person a rule on either side names for
1373
+ * the verb, as a grant or as a denial, and every holder either side lists;
1374
+ * each is put to both sides' resolvers as the gates would put it. The holder
1375
+ * lists alone are not enough: they name grants, so they miss a group still
1376
+ * covered by `everyone`, and they cannot show someone whose only access was
1377
+ * `everyone` and whom a rule on the far side denies by name.
1378
+ */
1379
+ function holderChangesFor(verb: 'read' | 'write', here: MoveSide, there: MoveSide): HolderChanges {
1380
+ const collectives = new Map<string, { holder: ResolvedPrincipal; keys: ReadonlySet<string> }>();
1381
+ const people = new Set<string>();
1382
+ const addCollective = (holder: ResolvedPrincipal, keys: ReadonlySet<string>) => {
1383
+ const id = `${holder.kind}\0${holder.name.toLowerCase()}`;
1384
+ if (!collectives.has(id)) collectives.set(id, { holder, keys });
1385
+ };
1386
+ for (const side of [here, there]) {
1387
+ const held = eligibleHoldersResolved(side.model, verb, side.path, side.own);
1388
+ for (const holder of held.principals) addCollective(holder, keysOfHolder(side.model, holder));
1389
+ for (const user of held.users) people.add(user.email);
1390
+ const { byRole, byEmail } = resolveAtPath(side.model, verb, side.path, side.own);
1391
+ for (const token of byRole.keys()) {
1392
+ const record = side.model.roles.byCanonical.get(token);
1393
+ addCollective(
1394
+ { name: record ? record.displayName : token, kind: record?.kind ?? 'role' },
1395
+ principalKeysOfToken(side.model, token),
1396
+ );
1397
+ }
1398
+ for (const email of byEmail.keys()) people.add(email);
1399
+ }
1400
+ const lose: HolderChange[] = [];
1401
+ const gain: HolderChange[] = [];
1402
+ const record = (had: boolean, has: boolean, change: HolderChange) => {
1403
+ if (had && !has) lose.push(change);
1404
+ if (!had && has) gain.push(change);
1405
+ };
1406
+ for (const { holder, keys } of collectives.values()) {
1407
+ record(
1408
+ hasPermissionForKeys(here.model, verb, keys, here.path, here.own),
1409
+ hasPermissionForKeys(there.model, verb, keys, there.path, there.own),
1410
+ { kind: holder.kind, name: holder.name, verb },
1411
+ );
1412
+ }
1413
+ for (const email of [...people].sort()) {
1414
+ record(
1415
+ hasPermissionResolved(here.model, verb, email, here.path, here.own),
1416
+ hasPermissionResolved(there.model, verb, email, there.path, there.own),
1417
+ { kind: 'person', name: email, email, verb },
1418
+ );
1419
+ }
1420
+ return { lose, gain };
1421
+ }
1422
+
1288
1423
  /**
1289
1424
  * Every individual the model names: `roles.yaml` members, emails named inline
1290
1425
  * at this path's scope, and the deployment owner (bound by every rule that
@@ -1656,7 +1791,10 @@ export class AccessControlService implements IAccessControl {
1656
1791
  const model = await this.loadModel(workspaceId);
1657
1792
  const email = canonicalEmail(userEmail);
1658
1793
  const names: string[] = [];
1659
- for (const principal of model.roles.byCanonical.values()) {
1794
+ // Each group once, through its `group/` alias (the bare key, when the
1795
+ // group owns it, is the same record).
1796
+ for (const [key, principal] of model.roles.byCanonical) {
1797
+ if (!key.startsWith(GROUP_TOKEN_PREFIX)) continue;
1660
1798
  if (principal.kind === 'group' && principal.emails.has(email)) names.push(principal.displayName);
1661
1799
  }
1662
1800
  return names;
@@ -1856,6 +1994,32 @@ export class AccessControlService implements IAccessControl {
1856
1994
  };
1857
1995
  }
1858
1996
 
1997
+ /** Who loses and who gains `read` or `write` in a move (see the interface). */
1998
+ async prospectiveHolderChanges(
1999
+ workspaceId: string,
2000
+ fromPath: string,
2001
+ toPath: string,
2002
+ ): Promise<HolderChanges> {
2003
+ const model = await this.loadModel(workspaceId);
2004
+ const repoDir = await this.repoDir(workspaceId);
2005
+ const { isFolder, ownHere, ownThere, landing } =
2006
+ await this.relocationSubject(repoDir, fromPath, toPath);
2007
+ const here: MoveSide = { model, path: fromPath, own: ownHere };
2008
+ const there: MoveSide = {
2009
+ model: withRelocatedAccessFiles(model, fromPath, toPath, { sourceRemains: false, sourceIsFolder: isFolder, landing }),
2010
+ path: toPath,
2011
+ own: ownThere,
2012
+ };
2013
+ const lose: HolderChange[] = [];
2014
+ const gain: HolderChange[] = [];
2015
+ for (const verb of ['read', 'write'] as const) {
2016
+ const changes = holderChangesFor(verb, here, there);
2017
+ lose.push(...changes.lose);
2018
+ gain.push(...changes.gain);
2019
+ }
2020
+ return { lose, gain };
2021
+ }
2022
+
1859
2023
  /**
1860
2024
  * The caller's own verbs at `toPath` as they will be once `fromPath` has
1861
2025
  * been moved or copied there (see the interface).
@@ -1879,11 +2043,11 @@ export class AccessControlService implements IAccessControl {
1879
2043
  userEmail: string,
1880
2044
  fromPath: string,
1881
2045
  toPath: string,
1882
- opts?: { sourceRemains?: boolean },
2046
+ opts?: { sourceRemains?: boolean; sourceContent?: string },
1883
2047
  ): Promise<PathVerbs> {
1884
2048
  const repoDir = await this.repoDir(workspaceId);
1885
2049
  const { isFolder, ownThere: own, landing } =
1886
- await this.relocationSubject(repoDir, fromPath, toPath);
2050
+ await this.relocationSubject(repoDir, fromPath, toPath, opts?.sourceContent);
1887
2051
  const model = withRelocatedAccessFiles(
1888
2052
  await this.loadModel(workspaceId),
1889
2053
  fromPath,
@@ -2067,9 +2231,9 @@ export class AccessControlService implements IAccessControl {
2067
2231
  }
2068
2232
  // Roles = the built-in `everyone` role plus every declared role's display
2069
2233
  // name — ROLE principals only, never groups. Each role is enumerated via
2070
- // its `role/<canonical>` alias key, which exists exactly once per role
2071
- // (the bare key may be owned by a same-named group under group-first
2072
- // precedence, and merged group entries must not appear here). `everyone`
2234
+ // its `role/<canonical>` alias key, which exists exactly once per role;
2235
+ // each group likewise via its `group/<canonical>` alias (a group that
2236
+ // owns its bare key is registered under both, as one record). `everyone`
2073
2237
  // is surfaced so the share UI can grant public read; the grant route
2074
2238
  // gates it to the `read` verb only (write/owner/download everyone stay a
2075
2239
  // direct-access.md edit).
@@ -2082,7 +2246,7 @@ export class AccessControlService implements IAccessControl {
2082
2246
  const plugins = new Map<string, string>();
2083
2247
  for (const [key, principal] of model.roles.byCanonical) {
2084
2248
  if (key.startsWith(ROLE_TOKEN_PREFIX)) roles.push(principal.displayName);
2085
- else if (principal.kind === 'group') groups.push(principal.displayName);
2249
+ else if (key.startsWith(GROUP_TOKEN_PREFIX) && principal.kind === 'group') groups.push(principal.displayName);
2086
2250
  else if (
2087
2251
  principal.kind === 'plugin' &&
2088
2252
  principal.pluginName &&
@@ -2515,15 +2679,20 @@ export class AccessControlService implements IAccessControl {
2515
2679
  * in the new (body-governed) format, where its frontmatter is self-access —
2516
2680
  * see {@link accessMdSelfEntries}. A legacy `access.md` still yields null:
2517
2681
  * its frontmatter IS the folder rules the dir chain already applies.
2682
+ *
2683
+ * Any other file declares them in its frontmatter alone, so only as much of
2684
+ * it is read as the frontmatter takes (see {@link frontmatterHead}): a batch
2685
+ * over a tree of megabyte transcripts reads a line of each, not the tree.
2518
2686
  */
2519
2687
  private async readOwnEntries(
2520
2688
  repoDir: string,
2521
2689
  relativePath: string,
2522
2690
  ): Promise<OwnEntries | null> {
2523
2691
  if (relativePath === 'roles.yaml') return null;
2692
+ const file = path.join(repoDir, relativePath);
2524
2693
  let text: string;
2525
2694
  try {
2526
- text = await fs.readFile(path.join(repoDir, relativePath), 'utf-8');
2695
+ text = isAccessMdPath(relativePath) ? await fs.readFile(file, 'utf-8') : await frontmatterHead(file);
2527
2696
  } catch {
2528
2697
  return null;
2529
2698
  }
@@ -2555,11 +2724,18 @@ export class AccessControlService implements IAccessControl {
2555
2724
  * Only absence is an answer (see `isAbsence`): a probe that failed on
2556
2725
  * permissions or I/O does not say "this is a file", and reading a folder as
2557
2726
  * one would resolve it through frontmatter it does not have.
2727
+ *
2728
+ * `sourceContent` is the text of a source that is NO repository path — a
2729
+ * file staged in the workspace's `tmp/` folder, which `copy_file` lands in
2730
+ * the repository. Nothing is probed on disk for it: it is one file by the
2731
+ * ref's own rule, and its text is read exactly as a repository file's is,
2732
+ * so the copy previews the rules it carries in.
2558
2733
  */
2559
2734
  private async relocationSubject(
2560
2735
  repoDir: string,
2561
2736
  fromPath: string,
2562
2737
  toPath: string,
2738
+ sourceContent?: string,
2563
2739
  ): Promise<{
2564
2740
  isFolder: boolean;
2565
2741
  ownHere: OwnEntries | null;
@@ -2567,19 +2743,21 @@ export class AccessControlService implements IAccessControl {
2567
2743
  landing: AccessFile | null;
2568
2744
  }> {
2569
2745
  const nothing = { isFolder: false, ownHere: null, ownThere: null, landing: null };
2570
- const fromStat = await fs.lstat(path.join(repoDir, fromPath)).catch((err: unknown) => {
2571
- if (isAbsence(err)) return null;
2572
- throw err;
2573
- });
2574
- if (fromStat?.isSymbolicLink() === true) return nothing;
2575
- if (fromStat?.isDirectory() === true) return { ...nothing, isFolder: true };
2746
+ if (sourceContent === undefined) {
2747
+ const fromStat = await fs.lstat(path.join(repoDir, fromPath)).catch((err: unknown) => {
2748
+ if (isAbsence(err)) return null;
2749
+ throw err;
2750
+ });
2751
+ if (fromStat?.isSymbolicLink() === true) return nothing;
2752
+ if (fromStat?.isDirectory() === true) return { ...nothing, isFolder: true };
2753
+ }
2576
2754
  // `roles.yaml` carries no own-entries wherever it sits, exactly as
2577
2755
  // `readOwnEntries` has it; a read that fails for any reason is no rules,
2578
2756
  // which is what the gates would say about the same file.
2579
2757
  if (fromPath === 'roles.yaml' || toPath === 'roles.yaml') return nothing;
2580
2758
  let text: string;
2581
2759
  try {
2582
- text = await fs.readFile(path.join(repoDir, fromPath), 'utf-8');
2760
+ text = sourceContent ?? (await fs.readFile(path.join(repoDir, fromPath), 'utf-8'));
2583
2761
  } catch {
2584
2762
  return nothing;
2585
2763
  }