@bevel-software/platform-core-backend 0.33.0 → 0.34.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 (497) hide show
  1. package/agent-guide/access-control.md +19 -8
  2. package/agent-guide/html-views.md +231 -5
  3. package/agent-guide/tool-manuals.md +1 -1
  4. package/dist/core/catalog-revision.d.ts +12 -1
  5. package/dist/core/catalog-revision.d.ts.map +1 -1
  6. package/dist/core/catalog-revision.js +23 -3
  7. package/dist/core/catalog-revision.js.map +1 -1
  8. package/dist/core/core-ports.d.ts +41 -0
  9. package/dist/core/core-ports.d.ts.map +1 -1
  10. package/dist/core/core-ports.js.map +1 -1
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +14 -7
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +16 -5
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +37 -12
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +16 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -1
  20. package/dist/core/lifecycle.js +12 -2
  21. package/dist/core/lifecycle.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/modules/access/access-control.interface.d.ts +17 -9
  26. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  27. package/dist/modules/access/access-control.service.d.ts +12 -2
  28. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.js +164 -75
  30. package/dist/modules/access/access-control.service.js.map +1 -1
  31. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  32. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  33. package/dist/modules/access/access-mutation.service.js +48 -51
  34. package/dist/modules/access/access-mutation.service.js.map +1 -1
  35. package/dist/modules/access/access-view.d.ts.map +1 -1
  36. package/dist/modules/access/access-view.js +12 -11
  37. package/dist/modules/access/access-view.js.map +1 -1
  38. package/dist/modules/access/access.routes.d.ts +3 -0
  39. package/dist/modules/access/access.routes.d.ts.map +1 -1
  40. package/dist/modules/access/access.routes.js +35 -29
  41. package/dist/modules/access/access.routes.js.map +1 -1
  42. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  43. package/dist/modules/access/groups-admin.routes.js +4 -2
  44. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  45. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  46. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  47. package/dist/modules/access/groups-admin.service.js +80 -38
  48. package/dist/modules/access/groups-admin.service.js.map +1 -1
  49. package/dist/modules/access/groups-edit.d.ts +1 -1
  50. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  51. package/dist/modules/access/groups-edit.js +1 -1
  52. package/dist/modules/access/groups-edit.js.map +1 -1
  53. package/dist/modules/access/reference-scan.d.ts +116 -24
  54. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  55. package/dist/modules/access/reference-scan.js +305 -44
  56. package/dist/modules/access/reference-scan.js.map +1 -1
  57. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  58. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  59. package/dist/modules/access/roles-admin.service.js +32 -23
  60. package/dist/modules/access/roles-admin.service.js.map +1 -1
  61. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  62. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  63. package/dist/modules/access-model/access-grammar.js +60 -20
  64. package/dist/modules/access-model/access-grammar.js.map +1 -1
  65. package/dist/modules/access-model/access-splice.d.ts +22 -18
  66. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  67. package/dist/modules/access-model/access-splice.js +20 -14
  68. package/dist/modules/access-model/access-splice.js.map +1 -1
  69. package/dist/modules/access-model/group-files.d.ts +1 -0
  70. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  71. package/dist/modules/access-model/group-files.js +5 -1
  72. package/dist/modules/access-model/group-files.js.map +1 -1
  73. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  74. package/dist/modules/agent-guide/agent-guide.tools.js +13 -3
  75. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  76. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  77. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  78. package/dist/modules/agent-instructions/compose.js +3 -1
  79. package/dist/modules/agent-instructions/compose.js.map +1 -1
  80. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  81. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  82. package/dist/modules/agent-instructions/shared-file-rules.js +16 -1
  83. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  84. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  85. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  86. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  87. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  88. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  89. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  90. package/dist/modules/agent-scope/connection-scope.js +165 -0
  91. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  92. package/dist/modules/agent-scope/index.d.ts +3 -0
  93. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  94. package/dist/modules/agent-scope/index.js +3 -0
  95. package/dist/modules/agent-scope/index.js.map +1 -0
  96. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  97. package/dist/modules/audit/agent-audit.service.js +2 -0
  98. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  99. package/dist/modules/audit/audit.contract.d.ts +6 -0
  100. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  101. package/dist/modules/audit/audit.contract.js.map +1 -1
  102. package/dist/modules/auth/account-admission.d.ts +12 -0
  103. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  104. package/dist/modules/auth/account-admission.js +15 -0
  105. package/dist/modules/auth/account-admission.js.map +1 -1
  106. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  107. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  108. package/dist/modules/auth/auth.middleware.js +42 -4
  109. package/dist/modules/auth/auth.middleware.js.map +1 -1
  110. package/dist/modules/auth/auth.routes.d.ts +13 -9
  111. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  112. package/dist/modules/auth/auth.routes.js +35 -29
  113. package/dist/modules/auth/auth.routes.js.map +1 -1
  114. package/dist/modules/auth/auth.service.d.ts +55 -6
  115. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  116. package/dist/modules/auth/auth.service.js +128 -34
  117. package/dist/modules/auth/auth.service.js.map +1 -1
  118. package/dist/modules/auth/index.d.ts +1 -1
  119. package/dist/modules/auth/index.d.ts.map +1 -1
  120. package/dist/modules/auth/index.js +1 -1
  121. package/dist/modules/auth/index.js.map +1 -1
  122. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  123. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  124. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  125. package/dist/modules/database/core-schema.d.ts +68 -0
  126. package/dist/modules/database/core-schema.d.ts.map +1 -1
  127. package/dist/modules/database/core-schema.js +33 -0
  128. package/dist/modules/database/core-schema.js.map +1 -1
  129. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  130. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  131. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  132. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  133. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  134. package/dist/modules/embed/embed.tools.js +5 -0
  135. package/dist/modules/embed/embed.tools.js.map +1 -1
  136. package/dist/modules/kb-fs/index.d.ts +2 -0
  137. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  138. package/dist/modules/kb-fs/index.js +1 -0
  139. package/dist/modules/kb-fs/index.js.map +1 -1
  140. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  141. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  142. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  143. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  144. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  145. package/dist/modules/kb-fs/repo-path.js +22 -7
  146. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  147. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  148. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  149. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  150. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  153. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  154. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  155. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  156. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  157. package/dist/modules/mcp/mcp.routes.js +39 -4
  158. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  159. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  160. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  161. package/dist/modules/mcp/mcp.service.js +68 -13
  162. package/dist/modules/mcp/mcp.service.js.map +1 -1
  163. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  164. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  165. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  167. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  168. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  169. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  171. package/dist/modules/plugins/index.d.ts +1 -1
  172. package/dist/modules/plugins/index.d.ts.map +1 -1
  173. package/dist/modules/plugins/index.js.map +1 -1
  174. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  175. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-links.js.map +1 -1
  177. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  178. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  179. package/dist/modules/plugins/plugins.routes.js +32 -5
  180. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  181. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  182. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  183. package/dist/modules/secrets-vault/secrets-vault.routes.js +15 -3
  184. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  185. package/dist/modules/skills/index.d.ts +3 -3
  186. package/dist/modules/skills/index.d.ts.map +1 -1
  187. package/dist/modules/skills/index.js +0 -2
  188. package/dist/modules/skills/index.js.map +1 -1
  189. package/dist/modules/skills/skills.contract.d.ts +16 -0
  190. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  192. package/dist/modules/skills/skills.service.js +6 -1
  193. package/dist/modules/skills/skills.service.js.map +1 -1
  194. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  195. package/dist/modules/skills/skills.tools.js +24 -12
  196. package/dist/modules/skills/skills.tools.js.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  198. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  201. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  202. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  203. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  204. package/dist/modules/tool-auth/index.d.ts +1 -1
  205. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  206. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  207. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  208. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  209. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  210. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  211. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  212. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  213. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  214. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  215. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  216. package/dist/modules/tool-helpers/tool-context.js +3 -0
  217. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  218. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  219. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  220. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  221. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  222. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  223. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  224. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  225. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  226. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  227. package/dist/modules/tool-helpers/validate-token.js +1 -1
  228. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  229. package/dist/modules/tool-manuals/index.d.ts +3 -3
  230. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  231. package/dist/modules/tool-manuals/index.js +0 -2
  232. package/dist/modules/tool-manuals/index.js.map +1 -1
  233. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +19 -6
  234. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  235. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  236. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  238. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  239. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +13 -6
  240. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  241. package/dist/modules/tool-manuals/tool-manuals.service.js +23 -15
  242. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  243. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.tools.js +25 -10
  245. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  246. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  247. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  248. package/dist/modules/tool-registry/manual.routes.js +27 -7
  249. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  250. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  251. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  252. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  253. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  254. package/dist/modules/workflow/agent-tools/workflow.tools.js +14 -9
  255. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  256. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  257. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  258. package/dist/modules/workflow/git/git.service.js +20 -35
  259. package/dist/modules/workflow/git/git.service.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +3 -0
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  265. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  266. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  267. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  268. package/dist/modules/workspace/index.d.ts +8 -0
  269. package/dist/modules/workspace/index.d.ts.map +1 -1
  270. package/dist/modules/workspace/index.js +8 -0
  271. package/dist/modules/workspace/index.js.map +1 -1
  272. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  273. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  274. package/dist/modules/workspace/nul-scan.js +55 -0
  275. package/dist/modules/workspace/nul-scan.js.map +1 -0
  276. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  277. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  278. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  279. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  280. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  281. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  282. package/dist/modules/workspace/ripgrep-search.js +657 -0
  283. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  284. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  285. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  286. package/dist/modules/workspace/ripgrep-version.js +31 -0
  287. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  288. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  289. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  290. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  291. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  292. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  293. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  294. package/dist/modules/workspace/worker-pool.js.map +1 -1
  295. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  296. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  297. package/dist/modules/workspace/workspace-tmp.js +435 -0
  298. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  299. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  300. package/dist/modules/workspace/workspace.routes.js +5 -26
  301. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  302. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  303. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  304. package/dist/modules/workspace/workspace.service.js +86 -6
  305. package/dist/modules/workspace/workspace.service.js.map +1 -1
  306. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  307. package/dist/modules/workspace/workspace.tools.js +460 -116
  308. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  309. package/dist/shared/path-order.d.ts +28 -8
  310. package/dist/shared/path-order.d.ts.map +1 -1
  311. package/dist/shared/path-order.js +37 -8
  312. package/dist/shared/path-order.js.map +1 -1
  313. package/migrations/0022_session_version.sql +6 -0
  314. package/migrations/0023_agent_path.sql +3 -0
  315. package/migrations/meta/0022_snapshot.json +2360 -0
  316. package/migrations/meta/0023_snapshot.json +2378 -0
  317. package/migrations/meta/_journal.json +14 -0
  318. package/package.json +3 -3
  319. package/src/__tests__/pg-scratch.test.ts +69 -0
  320. package/src/__tests__/pg-scratch.ts +31 -0
  321. package/src/__tests__/slow-ripgrep.ts +65 -0
  322. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  323. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  324. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  325. package/src/core/__tests__/gated-boot.test.ts +8 -1
  326. package/src/core/__tests__/lifecycle.test.ts +46 -13
  327. package/src/core/catalog-revision.ts +24 -2
  328. package/src/core/core-ports.ts +42 -0
  329. package/src/core/create-core-server.ts +13 -0
  330. package/src/core/create-core-services.ts +49 -12
  331. package/src/core/lifecycle.ts +26 -2
  332. package/src/index.ts +1 -1
  333. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  334. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  335. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  336. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  337. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  338. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  339. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  340. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  341. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  342. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  343. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  344. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  345. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  346. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  347. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  348. package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -0
  349. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  350. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  351. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  352. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  353. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  354. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  355. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  356. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  357. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  358. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  359. package/src/modules/access/access-control.interface.ts +17 -10
  360. package/src/modules/access/access-control.service.ts +158 -72
  361. package/src/modules/access/access-mutation.service.ts +48 -53
  362. package/src/modules/access/access-view.ts +12 -10
  363. package/src/modules/access/access.routes.ts +43 -33
  364. package/src/modules/access/groups-admin.routes.ts +4 -2
  365. package/src/modules/access/groups-admin.service.ts +114 -44
  366. package/src/modules/access/groups-edit.ts +1 -1
  367. package/src/modules/access/reference-scan.ts +319 -46
  368. package/src/modules/access/roles-admin.service.ts +43 -26
  369. package/src/modules/access/synced-groups-writer.ts +1 -1
  370. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  371. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  372. package/src/modules/access-model/access-grammar.ts +65 -20
  373. package/src/modules/access-model/access-splice.ts +31 -23
  374. package/src/modules/access-model/group-files.ts +5 -0
  375. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +31 -0
  376. package/src/modules/agent-guide/agent-guide.tools.ts +13 -2
  377. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +1 -0
  378. package/src/modules/agent-instructions/compose.ts +3 -1
  379. package/src/modules/agent-instructions/shared-file-rules.ts +17 -1
  380. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  381. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  382. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  383. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  384. package/src/modules/agent-scope/connection-scope.ts +199 -0
  385. package/src/modules/agent-scope/index.ts +15 -0
  386. package/src/modules/audit/agent-audit.service.ts +2 -0
  387. package/src/modules/audit/audit.contract.ts +6 -0
  388. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  389. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  390. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  391. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  392. package/src/modules/auth/account-admission.ts +19 -0
  393. package/src/modules/auth/auth.middleware.ts +45 -4
  394. package/src/modules/auth/auth.routes.ts +46 -29
  395. package/src/modules/auth/auth.service.ts +135 -34
  396. package/src/modules/auth/index.ts +2 -0
  397. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  398. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  399. package/src/modules/database/core-schema.ts +33 -0
  400. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  401. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  402. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  403. package/src/modules/embed/embed.tools.ts +5 -0
  404. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  405. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  406. package/src/modules/kb-fs/index.ts +2 -0
  407. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  408. package/src/modules/kb-fs/repo-path.ts +33 -7
  409. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  410. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  411. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  412. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  413. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  414. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  415. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  416. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  417. package/src/modules/mcp/mcp.routes.ts +39 -3
  418. package/src/modules/mcp/mcp.service.ts +107 -17
  419. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  420. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  421. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  422. package/src/modules/plugins/index.ts +1 -1
  423. package/src/modules/plugins/plugin-links.ts +19 -2
  424. package/src/modules/plugins/plugins.routes.ts +35 -4
  425. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  426. package/src/modules/secrets-vault/secrets-vault.routes.ts +23 -3
  427. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  428. package/src/modules/skills/index.ts +7 -2
  429. package/src/modules/skills/skills.contract.ts +17 -0
  430. package/src/modules/skills/skills.service.ts +5 -1
  431. package/src/modules/skills/skills.tools.ts +44 -13
  432. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  433. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  434. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  435. package/src/modules/tool-auth/index.ts +1 -1
  436. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  437. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  438. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  439. package/src/modules/tool-helpers/tool-context.ts +10 -0
  440. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  441. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  442. package/src/modules/tool-helpers/validate-token.ts +4 -2
  443. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  444. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  445. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  446. package/src/modules/tool-manuals/index.ts +6 -2
  447. package/src/modules/tool-manuals/tool-manuals.contract.ts +18 -6
  448. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  449. package/src/modules/tool-manuals/tool-manuals.service.ts +31 -15
  450. package/src/modules/tool-manuals/tool-manuals.tools.ts +28 -11
  451. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +2 -2
  452. package/src/modules/tool-registry/manual.routes.ts +26 -5
  453. package/src/modules/tool-registry/tool.contract.ts +7 -0
  454. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  455. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  457. package/src/modules/workflow/agent-tools/workflow.tools.ts +14 -9
  458. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  459. package/src/modules/workflow/git/git.service.ts +29 -48
  460. package/src/modules/workflow/workflow.service.ts +5 -0
  461. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  462. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  463. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  464. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  465. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  466. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  467. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  468. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  469. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -2
  470. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  471. package/src/modules/workspace/__tests__/workspace.tools.test.ts +393 -113
  472. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  473. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  474. package/src/modules/workspace/index.ts +20 -0
  475. package/src/modules/workspace/nul-scan.ts +50 -0
  476. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  477. package/src/modules/workspace/ripgrep-search.ts +715 -0
  478. package/src/modules/workspace/ripgrep-version.ts +35 -0
  479. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  480. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  481. package/src/modules/workspace/worker-pool.ts +3 -4
  482. package/src/modules/workspace/workspace-tmp.ts +457 -0
  483. package/src/modules/workspace/workspace.routes.ts +6 -27
  484. package/src/modules/workspace/workspace.service.ts +92 -15
  485. package/src/modules/workspace/workspace.tools.ts +500 -155
  486. package/src/shared/path-order.ts +37 -8
  487. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  488. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  489. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  490. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  491. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  492. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  493. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  494. package/dist/modules/workspace/bounded-matcher.js +0 -200
  495. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  496. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  497. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -0,0 +1,122 @@
1
+ /**
2
+ * QA reproduction (v0.28.0 hunt, decisions #312/#315): a Business user's
3
+ * agent proposed a group named `Admin` in groups.yaml, an Admin approved the
4
+ * change request, and every member of the group then received the Admin
5
+ * role's grants wherever an access rule said `Admin` — because a bare name
6
+ * meant the GROUP when a group and a role shared it.
7
+ *
8
+ * The rule now: a bare name means the ROLE when one of that name exists; the
9
+ * group is reachable only as `group/<Name>`. This pins that the approved
10
+ * group gives its members nothing the Admin role is granted.
11
+ */
12
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
13
+ import fs from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+ import { execFile } from 'node:child_process';
17
+ import { promisify } from 'node:util';
18
+
19
+ import { NodeFs } from '../../kb-fs/node-fs.js';
20
+ import type { WorkspaceService } from '../../workspace/index.js';
21
+ import { AccessControlService } from '../access-control.service.js';
22
+
23
+ const execFileAsync = promisify(execFile);
24
+ const KB_DIR = 'knowledge-base';
25
+ const WS = 'ws-group-named-admin';
26
+
27
+ const ADMIN = 'admin@x.io';
28
+ const BUSINESS = 'business@x.io';
29
+
30
+ const BEFORE: Record<string, string> = {
31
+ 'roles.yaml': `roles:\n Admin:\n - ${ADMIN}\n`,
32
+ 'groups.yaml': `groups:\n Sales:\n - ${BUSINESS}\n`,
33
+ // The root: Admin writes everything, Sales reads.
34
+ 'access.md': '---\nwrite:\n - Admin\n---\nread:\n - group/Sales\nwrite:\n - Admin\n',
35
+ // An admin-only folder, granted by the bare role name and closed to the rest.
36
+ 'Knowledge/Board/access.md': '---\nwrite:\n - Admin\n---\nread:\n - Admin\n - deny everyone\nwrite:\n - Admin\nowner:\n - Admin\n',
37
+ 'Knowledge/Board/Minutes.md': '# Minutes\n',
38
+ };
39
+
40
+ /** The change request's one edit: a group named Admin, holding the proposer. */
41
+ const PROPOSED_GROUPS = `groups:\n Sales:\n - ${BUSINESS}\n Admin:\n - ${BUSINESS}\n`;
42
+
43
+ describe('a group named Admin (QA reproduction)', () => {
44
+ let root: string;
45
+ let repo: string;
46
+ let svc: AccessControlService;
47
+ const git = (...args: string[]) => execFileAsync('git', ['-C', repo, ...args]);
48
+
49
+ beforeEach(async () => {
50
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-group-admin-'));
51
+ const workspaceDir = path.join(root, WS);
52
+ repo = path.join(workspaceDir, KB_DIR);
53
+ for (const [rel, text] of Object.entries(BEFORE)) {
54
+ await fs.mkdir(path.dirname(path.join(repo, rel)), { recursive: true });
55
+ await fs.writeFile(path.join(repo, rel), text);
56
+ }
57
+ await git('init', '-b', 'main');
58
+ await git('config', 'user.email', 'test@x.io');
59
+ await git('config', 'user.name', 'Test');
60
+ await git('add', '-A');
61
+ await git('commit', '-m', 'seed');
62
+
63
+ // The Business user's proposal, on its own branch…
64
+ await git('checkout', '-b', 'proposal/admin-group');
65
+ await fs.writeFile(path.join(repo, 'groups.yaml'), PROPOSED_GROUPS);
66
+ await git('commit', '-am', 'Add group Admin');
67
+ // …approved by an Admin and merged into main.
68
+ await git('checkout', 'main');
69
+ await git('merge', '--no-ff', '-m', 'Approve: Add group Admin', 'proposal/admin-group');
70
+
71
+ const workspaceService = {
72
+ getWorkspacePath: async () => workspaceDir,
73
+ ensureRemotesFetched: async () => undefined,
74
+ } as unknown as WorkspaceService;
75
+ svc = new AccessControlService(workspaceService, KB_DIR, new NodeFs());
76
+ });
77
+
78
+ afterEach(async () => {
79
+ await fs.rm(root, { recursive: true, force: true });
80
+ });
81
+
82
+ it('its members receive none of the Admin role grants', async () => {
83
+ // Line endings aside (Git may check the merge out with CRLF on Windows),
84
+ // main now holds the proposed groups file.
85
+ const merged = await fs.readFile(path.join(repo, 'groups.yaml'), 'utf-8');
86
+ expect(merged.replace(/\r\n/g, '\n')).toBe(PROPOSED_GROUPS);
87
+
88
+ // `read: Admin` / `write: Admin` / `owner: Admin` are the ROLE's.
89
+ expect(await svc.canRead(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
90
+ expect(await svc.canWrite(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
91
+ expect(await svc.canWrite(WS, BUSINESS, 'Knowledge/Doc.md')).toBe(false);
92
+ // …and the gate paths, which read the committed state, agree.
93
+ expect(await svc.canWriteAtRef(WS, 'main', BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
94
+ // The Admin capability is the role's alone.
95
+ expect(await svc.canWrite(WS, BUSINESS, 'roles.yaml')).toBe(false);
96
+ expect(await svc.adminEmails(WS)).toEqual([ADMIN]);
97
+
98
+ // The real Admin keeps everything, and Sales keeps what Sales had.
99
+ expect(await svc.canRead(WS, ADMIN, 'Knowledge/Board/Minutes.md')).toBe(true);
100
+ expect(await svc.canWrite(WS, ADMIN, 'Knowledge/Board/Minutes.md')).toBe(true);
101
+ expect(await svc.canRead(WS, BUSINESS, 'Knowledge/Doc.md')).toBe(true);
102
+
103
+ // The dialog's lists name the ROLE, never the group.
104
+ const readers = await svc.eligibleReaders(WS, 'Knowledge/Board/Minutes.md');
105
+ expect(readers.principals).toContainEqual({ name: 'Admin', kind: 'role' });
106
+ expect(readers.principals).not.toContainEqual({ name: 'Admin', kind: 'group' });
107
+ });
108
+
109
+ it('the group is reachable only by its explicit spelling, group/Admin', async () => {
110
+ await fs.writeFile(
111
+ path.join(repo, 'Knowledge/Board/access.md'),
112
+ '---\nwrite:\n - Admin\n---\nread:\n - Admin\n - group/Admin\n - deny everyone\nwrite:\n - Admin\n',
113
+ );
114
+ svc.invalidate(WS);
115
+ expect(await svc.canRead(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(true);
116
+ // Reading is all `group/Admin` was granted.
117
+ expect(await svc.canWrite(WS, BUSINESS, 'Knowledge/Board/Minutes.md')).toBe(false);
118
+ const readers = await svc.eligibleReaders(WS, 'Knowledge/Board/Minutes.md');
119
+ expect(readers.principals).toContainEqual({ name: 'Admin', kind: 'group' });
120
+ expect(readers.principals).toContainEqual({ name: 'Admin', kind: 'role' });
121
+ });
122
+ });
@@ -360,6 +360,56 @@ describe('AccessControlService.previewAccessAfterRelocation', () => {
360
360
  expect(await verbsAt(service(), 'Legal/note.md')).toEqual(after);
361
361
  });
362
362
 
363
+ /**
364
+ * A copy whose source is no repository path — a file staged in the
365
+ * workspace's `tmp/` folder — carries rules in by its text alone
366
+ * (`sourceContent`), read exactly as a repository file's would be. Checked,
367
+ * as every other case here, against the gates once the bytes really landed.
368
+ */
369
+ describe('a source outside the repository, previewed by its content', () => {
370
+ const REF = '__tmp__/staged.md';
371
+
372
+ it('its own frontmatter governs the file it lands as', async () => {
373
+ await write({ ...BASE, 'Work/access.md': rules(`write:\n - Mover <${MOVER}>\n`) });
374
+ const staged = rules(`read:\n - deny everyone\n - Other <other@x.io>\n`) + '# Staged\n';
375
+
376
+ const svc = service();
377
+ expect(await verbsAt(svc, 'Work/staged.md')).toMatchObject({ read: true, write: true });
378
+ const after = await svc.previewAccessAfterRelocation(
379
+ workspaceId, MOVER, REF, 'Work/staged.md', { sourceRemains: true, sourceContent: staged },
380
+ );
381
+ // The staged file shuts the caller out of what it becomes.
382
+ expect(after.read).toBe(false);
383
+
384
+ await write({ 'Work/staged.md': staged });
385
+ expect(await verbsAt(service(), 'Work/staged.md')).toEqual(after);
386
+ });
387
+
388
+ it('landing as an access.md, it gives the destination folder its rules', async () => {
389
+ await write({ ...BASE, 'Legal/note.md': '# Note\n' });
390
+ const staged = rules(`write:\n - Mover <${MOVER}>\nowner:\n - Mover <${MOVER}>\n`);
391
+
392
+ const svc = service();
393
+ expect(await verbsAt(svc, 'Legal/access.md')).toMatchObject({ write: false, owner: false });
394
+ const after = await svc.previewAccessAfterRelocation(
395
+ workspaceId, MOVER, REF, 'Legal/access.md', { sourceRemains: true, sourceContent: staged },
396
+ );
397
+ expect(after).toMatchObject({ write: true, owner: true });
398
+
399
+ await write({ 'Legal/access.md': staged });
400
+ expect(await verbsAt(service(), 'Legal/access.md')).toEqual(after);
401
+ });
402
+
403
+ it('nothing is read at the ref: a repository path spelled like it is not the source', async () => {
404
+ await write({ ...BASE, [REF]: rules(`read:\n - deny everyone\n`) + '# Not the staged file\n' });
405
+
406
+ const after = await service().previewAccessAfterRelocation(
407
+ workspaceId, MOVER, REF, 'Work/staged.md', { sourceRemains: true, sourceContent: '# Staged\n' },
408
+ );
409
+ expect(after).toEqual(await verbsAt(service(), 'Work/staged.md'));
410
+ });
411
+ });
412
+
363
413
  it('answers the caller\'s own verbs and nothing else, and writes nothing', async () => {
364
414
  await write({
365
415
  ...BASE,
@@ -5,6 +5,7 @@ import {
5
5
  AccessDeclarationsError,
6
6
  DECLARATION_SCAN_FILE_CAP,
7
7
  } from '../access-declarations.js';
8
+ import { registerAccessFrontmatterExtensions } from '../../access-model/access-grammar.js';
8
9
 
9
10
  /**
10
11
  * The declaration scan: WHICH files under a folder declare access rules, and
@@ -111,6 +112,24 @@ describe('listAccessDeclarationsUnder', () => {
111
112
  ]);
112
113
  });
113
114
 
115
+ it("reads verbs out of a distribution's configuration file (an .agent, once its kind is registered), governing the file itself", async () => {
116
+ // `.agent` is not core's: a distribution registers the kind at boot, as
117
+ // `registerAccessFrontmatterExtensions` is for, and the scan then reads it.
118
+ registerAccessFrontmatterExtensions(['.agent']);
119
+ const { overrides } = await scan({
120
+ [`${KB}/Agents/ops.agent`]: fm('name: ops\nplatformTools: [read_file]\nwrite:\n - Developer') + '\nNotes for people.\n',
121
+ }, 'Agents');
122
+
123
+ expect(overrides).toEqual([
124
+ {
125
+ path: 'Agents/ops.agent',
126
+ governs: 'Agents/ops.agent',
127
+ source: 'frontmatter',
128
+ entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'Developer' } }],
129
+ },
130
+ ]);
131
+ });
132
+
114
133
  it('ignores nodes whose frontmatter declares no access verb', async () => {
115
134
  const { overrides } = await scan({
116
135
  [`${KB}/Plugins/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
@@ -260,43 +260,75 @@ describe('group files as access principals', () => {
260
260
  expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
261
261
  });
262
262
 
263
- it('a bare name shared by a group and a role resolves to the GROUP (grant side)', async () => {
263
+ it('a bare name shared by a group and a role resolves to the ROLE (grant side)', async () => {
264
264
  const svc = await makeService({
265
265
  'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
266
266
  // The group "Engineering" collides with the ROLE Engineering.
267
267
  'groups.yaml': GROUPS_YAML_TEXT,
268
268
  'access.md': '---\nread:\n - Engineering\n---\n',
269
269
  });
270
- // Group-first precedence: the group's members read; the role's member no
271
- // longer resolves through the bare token (use role/Engineering for that).
272
- expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(true);
273
- expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(false);
274
- // The display surface resolves the bare token to the group too — and says
275
- // WHAT it is, so the dialog badges the row "Group", not "Role".
270
+ // Role-first precedence: the role's member reads; the group's members do
271
+ // not resolve through the bare token (group/Engineering is the group).
272
+ expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(true);
273
+ expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
274
+ // The display surface resolves the bare token to the role too — and says
275
+ // WHAT it is, so the dialog badges the row "Role", not "Group".
276
276
  const readers = await svc.eligibleReaders(workspaceId, 'Knowledge/Doc.md');
277
277
  expect(readers.roles).toContain('Engineering');
278
+ expect(readers.principals).toContainEqual({ name: 'Engineering', kind: 'role' });
279
+ expect(readers.principals).not.toContainEqual({ name: 'Engineering', kind: 'group' });
280
+ });
281
+
282
+ it('a bare name with no role of that name still resolves to the group (existing files keep working)', async () => {
283
+ const svc = await makeService({
284
+ 'roles.yaml': ROLES_YAML,
285
+ 'groups.yaml': GROUPS_YAML_TEXT,
286
+ 'access.md': '---\nread:\n - Engineering\n---\n',
287
+ });
288
+ expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(true);
289
+ const readers = await svc.eligibleReaders(workspaceId, 'Knowledge/Doc.md');
278
290
  expect(readers.principals).toContainEqual({ name: 'Engineering', kind: 'group' });
279
291
  });
280
292
 
281
- it('a bare-name DENY resolves to the group; role/<Name> reaches the role (writers included)', async () => {
293
+ it('group/<Name> always means the group, role/<Name> always the role', async () => {
282
294
  const svc = await makeService({
283
295
  'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
284
296
  'groups.yaml': GROUPS_YAML_TEXT,
285
297
  'access.md':
286
- '---\nread:\n - everyone\n - deny Engineering\nwrite:\n - role/Engineering\n---\n',
298
+ '---\nread:\n - everyone\n - deny group/Engineering\nwrite:\n - role/Engineering\nowner:\n - group/Sales\n---\n',
287
299
  });
288
- // The deny bites the GROUP's members, not the role's.
300
+ // The explicit group deny bites the GROUP's members, not the role's.
289
301
  expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
290
302
  expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(true);
291
- // The explicit token grants the ROLE write.
303
+ // The explicit role token grants the ROLE write.
292
304
  expect(await svc.canWrite(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(true);
293
305
  expect(await svc.canWrite(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(false);
294
- // eligibleWriters resolves the explicit token to the role's display name —
295
- // and carries kind 'role': the `role/` alias always means the role, even
296
- // with a same-named group in play.
297
306
  const writers = await svc.eligibleWriters(workspaceId, 'Knowledge/Doc.md');
298
- expect(writers.roles).toContain('Engineering');
299
307
  expect(writers.principals).toContainEqual({ name: 'Engineering', kind: 'role' });
308
+ // An unshared group name spelled group/ is the group too.
309
+ const owners = await svc.eligibleOwners(workspaceId, 'Knowledge/Doc.md');
310
+ expect(owners.principals).toContainEqual({ name: 'Sales', kind: 'group' });
311
+ });
312
+
313
+ it('a bare-name DENY resolves to the role when one exists, never the same-named group', async () => {
314
+ const svc = await makeService({
315
+ 'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
316
+ 'groups.yaml': GROUPS_YAML_TEXT,
317
+ 'access.md': '---\nread:\n - everyone\n - deny Engineering\n---\n',
318
+ });
319
+ expect(await svc.canRead(workspaceId, 'lead@x.io', 'Knowledge/Doc.md')).toBe(false);
320
+ // ada is in the Engineering GROUP only; a bare denial means the role.
321
+ expect(await svc.canRead(workspaceId, 'ada@x.io', 'Knowledge/Doc.md')).toBe(true);
322
+ });
323
+
324
+ it('a group/<Name> naming no group grants nobody', async () => {
325
+ const svc = await makeService({
326
+ 'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Reviewer:\n - rev@x.io\n`,
327
+ 'groups.yaml': GROUPS_YAML_TEXT,
328
+ 'access.md': '---\nread:\n - group/Reviewer\n---\n',
329
+ });
330
+ // A role of that name exists, but group/ never reaches a role.
331
+ expect(await svc.canRead(workspaceId, 'rev@x.io', 'Knowledge/Doc.md')).toBe(false);
300
332
  });
301
333
 
302
334
  it('role/Admin resolves to the ROLE even when a synced group is named Admin', async () => {
@@ -308,8 +340,9 @@ describe('group files as access principals', () => {
308
340
  // The explicit token is the role — the shadowing group gets nothing here.
309
341
  expect(await svc.canWrite(workspaceId, 'admin@x.io', 'Knowledge/Doc.md')).toBe(true);
310
342
  expect(await svc.canWrite(workspaceId, 'attacker@evil.io', 'Knowledge/Doc.md')).toBe(false);
311
- // The BARE token is the group now (group-first): its member reads.
312
- expect(await svc.canRead(workspaceId, 'attacker@evil.io', 'Knowledge/Doc.md')).toBe(true);
343
+ // The BARE token is the role too (role-first): the group's member gets nothing.
344
+ expect(await svc.canRead(workspaceId, 'attacker@evil.io', 'Knowledge/Doc.md')).toBe(false);
345
+ expect(await svc.canRead(workspaceId, 'admin@x.io', 'Knowledge/Doc.md')).toBe(true);
313
346
  // Admin CAPABILITY never leaks to the group: roles.yaml write is a pure
314
347
  // is-admin check keyed on the role, and admin-rescue keeps working.
315
348
  expect(await svc.canWrite(workspaceId, 'attacker@evil.io', 'roles.yaml')).toBe(false);
@@ -403,14 +436,14 @@ describe('group files as access principals', () => {
403
436
  });
404
437
 
405
438
  it('eligible holders keep BOTH kinds when a group and a role share a name', async () => {
406
- // A group named Engineering shadows the role Engineering. When a file
407
- // grants BOTH spellings — the bare token (the group's) and role/… (the
408
- // role's) — the eligible list must carry two entries, one per kind:
409
- // collapsing them hides the role's live role/<name> grant entirely.
439
+ // A group named Engineering shares the role Engineering's name. When a
440
+ // file grants BOTH — group/… (the group's) and the bare or role/… token
441
+ // (the role's) — the eligible list must carry two entries, one per kind:
442
+ // collapsing them hides one of the live grants entirely.
410
443
  const svc = await makeService({
411
444
  'roles.yaml': `roles:\n Admin:\n - admin@x.io\n Engineering:\n - lead@x.io\n`,
412
445
  'groups.yaml': GROUPS_YAML_TEXT,
413
- 'access.md': '---\nread:\n - Engineering\n - role/Engineering\n---\n',
446
+ 'access.md': '---\nread:\n - group/Engineering\n - Engineering\n---\n',
414
447
  });
415
448
  const readers = await svc.eligibleReaders(workspaceId, 'Knowledge/Doc.md');
416
449
  expect(readers.principals).toContainEqual({ name: 'Engineering', kind: 'group' });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { NodeFs } from '../../kb-fs/node-fs.js';
3
3
  import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
@@ -361,52 +361,47 @@ describe('AccessMutationService', () => {
361
361
  expect(await access.canWrite(WS, 'razvan@bevel.software', 'Sales/access.md')).toBe(true);
362
362
  });
363
363
 
364
- it('GROUP deny with a VANISHED group succeeds even when a same-named ROLE keeps a role/<Name> grant', async () => {
365
- // The false-negative this pins: the route pins tokenMatch:'exact' for a
366
- // GROUP principal, so the deny strips/denies ONLY the bare token. The
367
- // post-write effectiveness assert must judge the SAME exact identity —
368
- // with the group vanished the name reads "unshadowed", and the
369
- // alias-tolerant default would read the surviving `role/Product Team`
370
- // grant as the GROUP still having access, roll back a fully effective
364
+ it('GROUP deny with a VANISHED group succeeds even when a same-named ROLE keeps its grants', async () => {
365
+ // The false-negative this pins: a role named Product Team exists, so the
366
+ // service matches the GROUP's revoke/deny 'exact' — it strips and denies
367
+ // only `group/Product Team`. The post-write effectiveness assert must
368
+ // judge the SAME exact identity: with the group vanished, an
369
+ // alias-tolerant check would read the role's surviving bare and role/
370
+ // grants as the group still having access, roll back a fully effective
371
371
  // deny, and answer 409 deny-ineffective.
372
372
  await write(
373
373
  repo,
374
374
  'Sales/access.md',
375
- // The bare grant is the (now vanished) GROUP's; the role/ grant is the
376
- // same-named ROLE's own. No groups.yaml exists — the group is gone.
377
- '---\nwrite:\n - Product Team\n - role/Product Team\n---\n# Sales folder\n',
375
+ // The group/ grant is the (now vanished) GROUP's; the bare and role/
376
+ // grants are the same-named ROLE's. No groups.yaml — the group is gone.
377
+ '---\nwrite:\n - group/Product Team\n - Product Team\n - role/Product Team\n---\n# Sales folder\n',
378
378
  );
379
379
  access.invalidate(WS);
380
380
 
381
- const r = await mutation.denyHere(
382
- WS,
383
- 'folder',
384
- 'Sales',
385
- { kind: 'role', role: 'Product Team' },
386
- undefined,
387
- { tokenMatch: 'exact' },
388
- );
381
+ const r = await mutation.denyHere(WS, 'folder', 'Sales', { kind: 'role', role: 'group/Product Team' });
389
382
  expect(r.changed).toBe(true);
390
383
  access.invalidate(WS);
391
384
 
392
385
  const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
393
- // The bare (group) grant became a deny...
394
- expect(text).toContain('deny Product Team');
395
- // ...and the ROLE's own grant was never touched.
386
+ // The group's grant became a deny...
387
+ expect(text).toContain('deny group/Product Team');
388
+ // ...and the ROLE's own grants were never touched.
389
+ expect(text).toMatch(/^ {2}- Product Team$/m);
396
390
  expect(text).toContain('role/Product Team');
391
+ expect(text).not.toContain('deny Product Team');
397
392
  expect(text).not.toContain('deny role/Product Team');
398
- // The role still resolves its grant through the surviving role/ entry.
393
+ // The role still resolves its grant.
399
394
  const roleSources = await access.grantSources(WS, 'folder', 'Sales', {
400
395
  kind: 'role',
401
396
  role: 'role/Product Team',
402
397
  });
403
398
  expect(roleSources.write).toEqual([{ kind: 'direct' }]);
404
- // The exact bare token — the group's identity — holds nothing anymore.
399
+ // The group's exact token holds nothing anymore.
405
400
  const groupSources = await access.grantSources(
406
401
  WS,
407
402
  'folder',
408
403
  'Sales',
409
- { kind: 'role', role: 'Product Team' },
404
+ { kind: 'role', role: 'group/Product Team' },
410
405
  { tokenMatch: 'exact' },
411
406
  );
412
407
  expect(groupSources).toEqual({});
@@ -548,16 +543,17 @@ describe('AccessMutationService', () => {
548
543
  });
549
544
 
550
545
 
551
- // The four grant/revoke x shadowed/unshadowed cases, end-to-end through the
552
- // REAL resolver (kbPrincipals decides shadowing) — the token-kind family.
553
- describe('token-kind family — shadow-aware revoke, exact-token grant (real resolver)', () => {
546
+ // The grant/revoke x collided/uncollided cases, end-to-end through the REAL
547
+ // resolver (kbPrincipals decides who owns the bare name) — the token-kind family.
548
+ describe('token-kind family — role-first revoke, exact-token grant (real resolver)', () => {
554
549
  let root: string;
555
550
  let repo: string;
556
551
  let access: AccessControlService;
557
552
  let mutation: AccessMutationService;
558
553
 
559
554
  const roleP: Principal = { kind: 'role', role: 'role/Product Team' }; // the ROLE, explicit
560
- const bareP: Principal = { kind: 'role', role: 'Product Team' }; // the bare name (group when shadowed)
555
+ const bareP: Principal = { kind: 'role', role: 'Product Team' }; // the bare name (always the role here)
556
+ const groupP: Principal = { kind: 'role', role: 'group/Product Team' }; // the GROUP, explicit
561
557
 
562
558
  beforeEach(async () => {
563
559
  root = await mkTmpRoot();
@@ -575,64 +571,94 @@ describe('token-kind family — shadow-aware revoke, exact-token grant (real res
575
571
  await fs.rm(root, { recursive: true, force: true });
576
572
  });
577
573
 
578
- /** Shadow on = a GROUP named "Product Team" exists (owns the bare token). */
579
- async function shadow(on: boolean): Promise<void> {
574
+ /** Collision on = a GROUP named "Product Team" exists beside the role. */
575
+ async function collide(on: boolean): Promise<void> {
580
576
  if (on) {
581
- await write(repo, 'groups.yaml', 'groups:\n Product Team:\n - pat@x.io\n');
577
+ await write(repo, 'groups.yaml', 'groups:\n Product Team:\n - pat@x.io\n Design:\n - dee@x.io\n');
582
578
  }
583
579
  access.invalidate(WS);
584
580
  }
585
581
 
586
- it('GRANT shadowed: the group grant lands even though role/<Name> is already granted', async () => {
587
- await shadow(true);
588
- await write(repo, 'Sales/access.md', '---\nwrite:\n - role/Product Team\n---\n');
589
- const r = await mutation.grant(WS, 'folder', 'Sales', 'write', bareP);
590
- expect(r.changed).toBe(true); // exact-token idempotency: bare and role/ are different principals
582
+ it('GRANT collided: the group grant lands even though the role is already granted', async () => {
583
+ await collide(true);
584
+ await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
585
+ const r = await mutation.grant(WS, 'folder', 'Sales', 'write', groupP);
586
+ expect(r.changed).toBe(true); // exact-token idempotency: group/ is a different principal
591
587
  const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
592
- expect(text).toContain('- role/Product Team');
593
- expect(text.split('\n')).toContain(' - Product Team');
594
- // The group member now resolves write through the bare token.
588
+ expect(text.split('\n')).toContain(' - group/Product Team');
589
+ // The group member now resolves write through group/.
595
590
  access.invalidate(WS);
596
591
  expect(await access.canWrite(WS, 'pat@x.io', 'Sales/Deal.md')).toBe(true);
597
592
  });
598
593
 
599
- it('GRANT unshadowed: same-spelling grants stay idempotent', async () => {
600
- await shadow(false);
594
+ it('GRANT uncollided: same-spelling grants stay idempotent', async () => {
595
+ await collide(false);
601
596
  await write(repo, 'Sales/access.md', '---\nwrite:\n - role/Product Team\n---\n');
602
597
  const r = await mutation.grant(WS, 'folder', 'Sales', 'write', roleP);
603
598
  expect(r.changed).toBe(false);
604
599
  });
605
600
 
606
- it("REVOKE shadowed: revoking the ROLE leaves the group's bare grant intact", async () => {
607
- await shadow(true);
601
+ it("REVOKE on a model-load failure: a ROLE revoke stays exact and keeps the bare token", async () => {
602
+ await collide(true);
608
603
  await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
604
+ // A failed load answers empty principal lists: nothing says who owns the bare name.
605
+ vi.spyOn(access, 'kbPrincipals').mockResolvedValue({ roles: [], groups: [], plugins: [] } as never);
606
+ const r = await mutation.revoke(WS, 'folder', 'Sales', roleP, 'admin@x');
607
+ expect(r.changed).toBe(true);
608
+ const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
609
+ expect(text).not.toContain('role/Product Team');
610
+ expect(text.split('\n')).toContain(' - Product Team');
611
+ });
612
+
613
+ it("REVOKE collided: revoking the ROLE strips its bare and role/ spellings, never the group's", async () => {
614
+ await collide(true);
615
+ await write(
616
+ repo,
617
+ 'Sales/access.md',
618
+ '---\nwrite:\n - Product Team\n - role/Product Team\n - group/Product Team\n---\n',
619
+ );
609
620
  const r = await mutation.revoke(WS, 'folder', 'Sales', roleP, 'admin@x');
610
621
  expect(r.changed).toBe(true);
611
622
  const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
612
623
  expect(text).not.toContain('role/Product Team');
613
- expect(text.split('\n')).toContain(' - Product Team'); // the GROUP's token survives the role revoke
614
- // ...and the group member still resolves.
624
+ expect(text.split('\n')).not.toContain(' - Product Team');
625
+ expect(text.split('\n')).toContain(' - group/Product Team'); // the GROUP's token survives
615
626
  access.invalidate(WS);
616
627
  expect(await access.canWrite(WS, 'pat@x.io', 'Sales/Deal.md')).toBe(true);
628
+ expect(await access.canWrite(WS, 'felix@example.com', 'Sales/Deal.md')).toBe(false);
617
629
  });
618
630
 
619
- it("REVOKE shadowed: revoking the GROUP leaves the role's explicit grant intact", async () => {
620
- await shadow(true);
621
- await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
622
- const r = await mutation.revoke(WS, 'folder', 'Sales', bareP, 'admin@x');
631
+ it("REVOKE collided: revoking the GROUP strips group/ only — the bare name is the role's", async () => {
632
+ await collide(true);
633
+ await write(
634
+ repo,
635
+ 'Sales/access.md',
636
+ '---\nwrite:\n - Product Team\n - role/Product Team\n - group/Product Team\n---\n',
637
+ );
638
+ const r = await mutation.revoke(WS, 'folder', 'Sales', groupP, 'admin@x');
623
639
  expect(r.changed).toBe(true);
624
640
  const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
625
- expect(text).toContain('- role/Product Team'); // the ROLE's token survives the group revoke
626
- expect(text.split('\n')).not.toContain(' - Product Team');
627
- // The role member (felix, per roles.yaml) still resolves via role/.
641
+ expect(text).not.toContain('group/Product Team');
642
+ expect(text.split('\n')).toContain(' - Product Team');
643
+ expect(text).toContain('- role/Product Team');
628
644
  access.invalidate(WS);
629
645
  expect(await access.canWrite(WS, 'felix@example.com', 'Sales/Deal.md')).toBe(true);
630
- // The group member no longer does.
631
646
  expect(await access.canWrite(WS, 'pat@x.io', 'Sales/Deal.md')).toBe(false);
632
647
  });
633
648
 
634
- it('REVOKE unshadowed: revoking the role strips BOTH spellings (legacy bare cleanup)', async () => {
635
- await shadow(false);
649
+ it('REVOKE: a group no role shares a name with strips group/ AND its legacy bare spelling', async () => {
650
+ await collide(true);
651
+ await write(repo, 'Sales/access.md', '---\nwrite:\n - Design\n - group/Design\n---\n');
652
+ const r = await mutation.revoke(WS, 'folder', 'Sales', { kind: 'role', role: 'group/Design' }, 'admin@x');
653
+ expect(r.changed).toBe(true);
654
+ const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
655
+ expect(text).not.toContain('Design');
656
+ access.invalidate(WS);
657
+ expect(await access.canWrite(WS, 'dee@x.io', 'Sales/Deal.md')).toBe(false);
658
+ });
659
+
660
+ it('REVOKE uncollided: revoking the role strips BOTH spellings (legacy bare cleanup)', async () => {
661
+ await collide(false);
636
662
  await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
637
663
  const r = await mutation.revoke(WS, 'folder', 'Sales', roleP, 'admin@x');
638
664
  expect(r.changed).toBe(true);
@@ -642,12 +668,12 @@ describe('token-kind family — shadow-aware revoke, exact-token grant (real res
642
668
  expect(await access.canWrite(WS, 'felix@example.com', 'Sales/Deal.md')).toBe(false);
643
669
  });
644
670
 
645
- it('REVOKE unshadowed: a bare revoke (group since vanished) also strips both spellings', async () => {
646
- await shadow(false);
671
+ it('REVOKE uncollided: a bare revoke also strips both spellings', async () => {
672
+ await collide(false);
647
673
  await write(repo, 'Sales/access.md', '---\nwrite:\n - Product Team\n - role/Product Team\n---\n');
648
674
  const r = await mutation.revoke(WS, 'folder', 'Sales', bareP, 'admin@x');
649
675
  expect(r.changed).toBe(true);
650
676
  const text = await fs.readFile(path.join(repo, 'Sales/access.md'), 'utf-8');
651
- expect(text).not.toContain('Product Team'); // unshadowed: both spellings are the role
677
+ expect(text).not.toContain('Product Team'); // both spellings are the role
652
678
  });
653
679
  });
@@ -123,9 +123,11 @@ describe('restricting below a parent keeps the principal reported', () => {
123
123
  principal: Principal,
124
124
  picked: Partial<Record<Verb, boolean>> | null,
125
125
  ): Promise<void> {
126
+ // The mutation service derives its matching (no role shares GTM Team's
127
+ // name, so 'exact'); the read side is told the same.
126
128
  const opts = principal.kind === 'role' ? ({ tokenMatch: 'exact' } as const) : undefined;
127
129
  if (picked === null) {
128
- await mutation.denyHere(WS, 'folder', FOLDER, principal, undefined, opts);
130
+ await mutation.denyHere(WS, 'folder', FOLDER, principal, undefined);
129
131
  access.invalidate(WS);
130
132
  return;
131
133
  }
@@ -135,11 +137,11 @@ describe('restricting below a parent keeps the principal reported', () => {
135
137
  const src = await access.grantSources(WS, 'folder', FOLDER, toGrantPrincipal(principal), opts);
136
138
  const list = src[verb] ?? [];
137
139
  const localOnly = list.length > 0 && list.every((s) => s.kind === 'direct');
138
- if (localOnly) await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb, opts);
139
- else await mutation.denyHere(WS, 'folder', FOLDER, principal, verb, opts);
140
+ if (localOnly) await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb);
141
+ else await mutation.denyHere(WS, 'folder', FOLDER, principal, verb);
140
142
  access.invalidate(WS);
141
143
  if (localOnly && (await effective(principal))[verb]) {
142
- await mutation.denyHere(WS, 'folder', FOLDER, principal, verb, opts);
144
+ await mutation.denyHere(WS, 'folder', FOLDER, principal, verb);
143
145
  access.invalidate(WS);
144
146
  }
145
147
  }
@@ -153,7 +155,7 @@ describe('restricting below a parent keeps the principal reported', () => {
153
155
  opts,
154
156
  );
155
157
  if (!(denials[verb] ?? []).some((s) => s.kind === 'direct')) continue;
156
- await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb, opts);
158
+ await mutation.revoke(WS, 'folder', FOLDER, principal, 'me@x', verb);
157
159
  access.invalidate(WS);
158
160
  }
159
161
  for (const verb of minimalGrants(picked)) {
@@ -97,6 +97,7 @@ async function makeHarness(
97
97
  { emit: vi.fn() } as unknown as WorkflowEventBus,
98
98
  db,
99
99
  testKbContext({ kbDirName: KB }),
100
+ new NodeFs(),
100
101
  deploymentAdmins,
101
102
  ),
102
103
  );
@@ -11,6 +11,7 @@ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
11
11
  import { createAccessRoutes } from '../access.routes.js';
12
12
  import { usersDbDouble } from './users-db-double.js';
13
13
  import { testKbContext } from '../../../__tests__/kb-context.js';
14
+ import { NodeFs } from '../../kb-fs/node-fs.js';
14
15
 
15
16
  /**
16
17
  * HTTP contract for `POST /access/batch` — the verb dispatch. Existing callers
@@ -66,6 +67,7 @@ async function makeHarness(): Promise<Harness> {
66
67
  { emit: vi.fn() } as unknown as WorkflowEventBus,
67
68
  usersDbDouble(),
68
69
  testKbContext({ kbDirName: KB }),
70
+ new NodeFs(),
69
71
  ),
70
72
  );
71
73