@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
@@ -16,6 +16,7 @@ import { createAccessRoutes } from '../access.routes.js';
16
16
  import { usersDbDouble } from './users-db-double.js';
17
17
  import { AccessMutationError, AccessMutationService } from '../access-mutation.service.js';
18
18
  import { testKbContext } from '../../../__tests__/kb-context.js';
19
+ import { NodeFs } from '../../kb-fs/node-fs.js';
19
20
 
20
21
  /**
21
22
  * A file that cannot carry frontmatter is never written by an access mutation.
@@ -161,6 +162,7 @@ describe('access mutations on a file that cannot carry frontmatter', () => {
161
162
  { emit: vi.fn() } as unknown as WorkflowEventBus,
162
163
  usersDbDouble(),
163
164
  testKbContext({ kbDirName: KB }),
165
+ new NodeFs(),
164
166
  ),
165
167
  );
166
168
  server = await new Promise<Server>((resolve) => {
@@ -12,6 +12,7 @@ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
12
12
  import { createAccessRoutes } from '../access.routes.js';
13
13
  import { usersDbDouble } from './users-db-double.js';
14
14
  import { testKbContext } from '../../../__tests__/kb-context.js';
15
+ import { NodeFs } from '../../kb-fs/node-fs.js';
15
16
 
16
17
  /**
17
18
  * HTTP-level contract tests for GROUP and ROLE principals on the share
@@ -99,7 +100,7 @@ async function makeHarness(opts: {
99
100
  (req as unknown as { userId: string }).userId = USER.id;
100
101
  next();
101
102
  });
102
- app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB })));
103
+ app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB }), new NodeFs()));
103
104
 
104
105
  const server = await new Promise<Server>((resolve) => {
105
106
  const s = app.listen(0, () => resolve(s));
@@ -126,7 +127,7 @@ describe('group principals on the share surface', () => {
126
127
  body: JSON.stringify(body),
127
128
  });
128
129
 
129
- it('grants read to a known group — spliced as the bare-name token', async () => {
130
+ it('grants read to a known group — spliced as the explicit group/<Name> token', async () => {
130
131
  h = await makeHarness({ groups: ['GTM Team', 'Product'] });
131
132
  const res = await post('grant', {
132
133
  path: `${KB}/Sales`,
@@ -136,8 +137,8 @@ describe('group principals on the share surface', () => {
136
137
  });
137
138
  expect(res.status).toBe(200);
138
139
  const md = h.files.get(`${KB}/Sales/access.md`)!;
139
- expect(md).toContain('- GTM Team');
140
- expect(md).not.toContain('role/'); // a GROUP grant is the bare token
140
+ expect(md.split('\n')).toContain(' - group/GTM Team'); // never the bare name
141
+ expect(md).not.toContain('role/');
141
142
  });
142
143
 
143
144
  it('grants a ROLE as the explicit role/<Name> token', async () => {
@@ -190,7 +191,7 @@ describe('group principals on the share surface', () => {
190
191
  expect(await res.json()).toMatchObject({ kind: 'unknown-role', role: 'Ghost Role' });
191
192
  });
192
193
 
193
- it('a group sharing a role name is grantable — precedence resolves it (no refusal)', async () => {
194
+ it('a group sharing a role name is grantable — each kind is written with its prefix (no refusal)', async () => {
194
195
  h = await makeHarness({ roles: ['Everyone', 'Product'], groups: ['Product'] });
195
196
  const asGroup = await post('grant', {
196
197
  path: `${KB}/Sales`,
@@ -199,7 +200,7 @@ describe('group principals on the share surface', () => {
199
200
  principal: { kind: 'group', group: 'Product' },
200
201
  });
201
202
  expect(asGroup.status).toBe(200);
202
- expect(h.files.get(`${KB}/Sales/access.md`)).toContain('- Product');
203
+ expect(h.files.get(`${KB}/Sales/access.md`)).toContain('- group/Product');
203
204
  // The ROLE of the same name grants as the explicit token, side by side.
204
205
  const asRole = await post('grant', {
205
206
  path: `${KB}/Sales`,
@@ -249,7 +250,19 @@ describe('group principals on the share surface', () => {
249
250
  principal: { kind: 'group', group: 'Product' },
250
251
  });
251
252
  expect(res.status).toBe(200);
252
- expect(h.files.get(`${KB}/Sales/access.md`)).toContain('- Product');
253
+ expect(h.files.get(`${KB}/Sales/access.md`)).toContain('- group/Product');
254
+ });
255
+
256
+ it('a group named with its group/ prefix is written once-prefixed', async () => {
257
+ h = await makeHarness({ groups: ['GTM Team'] });
258
+ const res = await post('grant', {
259
+ path: `${KB}/Sales`,
260
+ kind: 'folder',
261
+ verb: 'read',
262
+ principal: { kind: 'group', group: 'group/GTM Team' },
263
+ });
264
+ expect(res.status).toBe(200);
265
+ expect(h.files.get(`${KB}/Sales/access.md`)!.split('\n')).toContain(' - group/GTM Team');
253
266
  });
254
267
 
255
268
  it('a non-admin granting to a group they are not in is refused exactly like an unknown group', async () => {
@@ -281,9 +294,11 @@ describe('group principals on the share surface', () => {
281
294
 
282
295
  it('revokes with a group principal (name-based, no existence check)', async () => {
283
296
  h = await makeHarness({
297
+ roles: ['Everyone'],
284
298
  groups: ['GTM Team'],
285
299
  files: {
286
- [`${KB}/Sales/access.md`]: '---\nread:\n - Vanished Team\n---\n',
300
+ // Both spellings: the legacy bare one and the explicit one.
301
+ [`${KB}/Sales/access.md`]: '---\nread:\n - Vanished Team\n - group/Vanished Team\n---\n',
287
302
  },
288
303
  });
289
304
  const res = await post('revoke', {
@@ -369,11 +384,11 @@ describe('token-kind family at the route boundary', () => {
369
384
  expect(md).toContain('- Admin');
370
385
  });
371
386
 
372
- it('granting a GROUP that shadows an already-granted role/<Name> is NOT swallowed as idempotent', async () => {
387
+ it('granting a GROUP that shares an already-granted role name is NOT swallowed as idempotent', async () => {
373
388
  h = await makeHarness({
374
389
  roles: ['Everyone', 'Product'],
375
390
  groups: ['Product'],
376
- files: { [`${KB}/Sales/access.md`]: '---\nread:\n - role/Product\n---\n' },
391
+ files: { [`${KB}/Sales/access.md`]: '---\nread:\n - role/Product\n - Product\n---\n' },
377
392
  });
378
393
  const res = await post('grant', {
379
394
  path: `${KB}/Sales`,
@@ -384,15 +399,15 @@ describe('token-kind family at the route boundary', () => {
384
399
  expect(res.status).toBe(200);
385
400
  const md = h.files.get(`${KB}/Sales/access.md`)!;
386
401
  expect(md).toContain('- role/Product');
387
- expect(md.split('\n')).toContain(' - Product'); // the group's bare token landed
402
+ expect(md.split('\n')).toContain(' - group/Product'); // the group's token landed
388
403
  });
389
404
 
390
- it('revoking the ROLE while a same-named GROUP is granted leaves the bare token (kbPrincipals-driven shadowing)', async () => {
405
+ it("revoking the ROLE while a same-named GROUP is granted leaves the group's token", async () => {
391
406
  h = await makeHarness({
392
407
  roles: ['Everyone', 'Product'],
393
408
  groups: ['Product'],
394
409
  files: {
395
- [`${KB}/Sales/access.md`]: '---\nwrite:\n - Product\n - role/Product\n---\n',
410
+ [`${KB}/Sales/access.md`]: '---\nwrite:\n - Product\n - role/Product\n - group/Product\n---\n',
396
411
  },
397
412
  });
398
413
  const res = await post('revoke', {
@@ -403,6 +418,27 @@ describe('token-kind family at the route boundary', () => {
403
418
  expect(res.status).toBe(200);
404
419
  const md = h.files.get(`${KB}/Sales/access.md`)!;
405
420
  expect(md).not.toContain('role/Product');
406
- expect(md.split('\n')).toContain(' - Product'); // the GROUP's grant survives
421
+ expect(md.split('\n')).not.toContain(' - Product'); // the bare name was the role's
422
+ expect(md.split('\n')).toContain(' - group/Product'); // the GROUP's grant survives
423
+ });
424
+
425
+ it("revoking the GROUP while a same-named ROLE is granted leaves the role's spellings", async () => {
426
+ h = await makeHarness({
427
+ roles: ['Everyone', 'Product'],
428
+ groups: ['Product'],
429
+ files: {
430
+ [`${KB}/Sales/access.md`]: '---\nwrite:\n - Product\n - role/Product\n - group/Product\n---\n',
431
+ },
432
+ });
433
+ const res = await post('revoke', {
434
+ path: `${KB}/Sales`,
435
+ kind: 'folder',
436
+ principal: { kind: 'group', group: 'Product' },
437
+ });
438
+ expect(res.status).toBe(200);
439
+ const md = h.files.get(`${KB}/Sales/access.md`)!;
440
+ expect(md).not.toContain('group/Product');
441
+ expect(md.split('\n')).toContain(' - Product');
442
+ expect(md).toContain('- role/Product');
407
443
  });
408
444
  });
@@ -12,6 +12,7 @@ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
12
12
  import { createAccessRoutes } from '../access.routes.js';
13
13
  import { usersDbDouble } from './users-db-double.js';
14
14
  import { testKbContext } from '../../../__tests__/kb-context.js';
15
+ import { NodeFs } from '../../kb-fs/node-fs.js';
15
16
 
16
17
  /**
17
18
  * HTTP contract for `GET /access/overrides`. The scan itself is covered in
@@ -98,7 +99,7 @@ async function makeHarness(opts: {
98
99
  });
99
100
  app.use(
100
101
  '/api',
101
- createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB })),
102
+ createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB }), new NodeFs()),
102
103
  );
103
104
 
104
105
  const server = await new Promise<Server>((resolve) => {
@@ -139,6 +139,7 @@ describe('access routes on a proposal branch (real git)', () => {
139
139
  { emit: () => {} } as unknown as WorkflowEventBus,
140
140
  usersDbDouble(),
141
141
  testKbContext({ kbDirName: KB }),
142
+ new NodeFs(),
142
143
  ),
143
144
  );
144
145
  server = await new Promise<Server>((resolve) => {
@@ -11,6 +11,7 @@ import type { WorkflowEventBus } from '../../workflow/event-bus.js';
11
11
  import type { Database } from '../../database/connection.js';
12
12
  import { createAccessRoutes } from '../access.routes.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 `GET /access/prospective` — what the move confirmation
@@ -69,6 +70,7 @@ async function makeHarness(opts: { canRead?: boolean } = {}): Promise<Harness> {
69
70
  { emit: vi.fn() } as unknown as WorkflowEventBus,
70
71
  {} as unknown as Database,
71
72
  testKbContext({ kbDirName: KB }),
73
+ new NodeFs(),
72
74
  ),
73
75
  );
74
76
 
@@ -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-level contract tests for the revoke route's CLASSIFICATION + MODE logic —
@@ -56,6 +57,8 @@ async function makeHarness(opts: {
56
57
  effectiveEmails?: Record<string, boolean>;
57
58
  /** Override the (otherwise-empty) eligibleWriters list so a principal shows as a row in resolvedView. */
58
59
  eligibleWriters?: { roles: string[]; users: { name: string; email: string }[] };
60
+ /** kbPrincipals' roles — empty (the default) reads as a model that failed to load. */
61
+ roles?: string[];
59
62
  }): Promise<Harness> {
60
63
  const files = new Map<string, string>(Object.entries(opts.files ?? {}));
61
64
 
@@ -79,8 +82,8 @@ async function makeHarness(opts: {
79
82
  invalidate: vi.fn(),
80
83
  // The mutation service consults the merged principal index to decide
81
84
  // exact-vs-name token matching on revoke (group shadowing). No groups in
82
- // these fixtures → unshadowed (name-level) matching, the legacy behavior.
83
- kbPrincipals: vi.fn(async () => ({ roles: [], groups: [], people: [] })),
85
+ // these fixtures; no roles (the default) reads as a failed load → exact.
86
+ kbPrincipals: vi.fn(async () => ({ roles: opts.roles ?? [], groups: [], people: [] })),
84
87
  // resolvedView() fans out to these after a successful mutation; harmless stubs.
85
88
  // canRead/canDownload/canOwner are ALSO consumed by denyHere's assert.
86
89
  canRead: vi.fn(async (_w: string, email: string) => effEmail(email, true)),
@@ -134,7 +137,7 @@ async function makeHarness(opts: {
134
137
  (req as unknown as { userId: string }).userId = USER.id;
135
138
  next();
136
139
  });
137
- app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB })));
140
+ app.use('/api', createAccessRoutes(accessControl, workspaceService, authService, authService, workflowService, eventBus, db, testKbContext({ kbDirName: KB }), new NodeFs()));
138
141
 
139
142
  const server = await new Promise<Server>((resolve) => {
140
143
  const s = app.listen(0, () => resolve(s));
@@ -285,16 +288,16 @@ describe('POST /access/revoke — route classification + modes', () => {
285
288
  expect(h.stubs.files.get(`${KB}/Sales/access.md`)).toContain('Admin'); // untouched
286
289
  });
287
290
 
288
- it('a GROUP revoke matches its exact bare token only — a VANISHED group never strips role/<Name>', async () => {
289
- // The file grants BOTH spellings of one name: the bare token (the
290
- // group's grant) and role/Product (the role's). The group has since
291
- // vanished from the active source (kbPrincipals reports no groups), so
292
- // the mutation layer's shadow probe reads "unshadowed" — name-level
293
- // matching would strip BOTH spellings, silently revoking the ROLE. The
294
- // route pins exact-token matching for group principals instead.
291
+ it('a GROUP revoke strips group/<Name> — a VANISHED group never strips the role spellings', async () => {
292
+ // The file grants every spelling of one name: the bare token and
293
+ // role/Product (the role's) and group/Product (the group's). The group
294
+ // has since vanished from the active source and the principal index
295
+ // says nothing about roles either (kbPrincipals reports none), so
296
+ // nothing vouches that the bare name is the group's: only the explicit
297
+ // group token goes.
295
298
  h = await makeHarness({
296
299
  files: {
297
- [`${KB}/Sales/access.md`]: '---\nwrite:\n - Product\n - role/Product\n---\n',
300
+ [`${KB}/Sales/access.md`]: '---\nwrite:\n - Product\n - role/Product\n - group/Product\n---\n',
298
301
  },
299
302
  grantSources: () => ({}),
300
303
  });
@@ -305,9 +308,10 @@ describe('POST /access/revoke — route classification + modes', () => {
305
308
  });
306
309
  expect(res.status).toBe(200);
307
310
  const text = h.stubs.files.get(`${KB}/Sales/access.md`)!;
308
- // The group's bare grant is gone; the role's explicit grant survives.
311
+ // The group's grant is gone; the role's spellings survive.
312
+ expect(text).not.toContain('group/Product');
309
313
  expect(text).toContain('- role/Product');
310
- expect(text.split('\n')).not.toContain(' - Product');
314
+ expect(text.split('\n')).toContain(' - Product');
311
315
  });
312
316
 
313
317
  it('a ROLE revoke still sweeps legacy bare spellings when nothing shadows the name', async () => {
@@ -318,6 +322,7 @@ describe('POST /access/revoke — route classification + modes', () => {
318
322
  [`${KB}/Sales/access.md`]: '---\nwrite:\n - Product\n - role/Product\n---\n',
319
323
  },
320
324
  grantSources: () => ({}),
325
+ roles: ['Everyone', 'Product'],
321
326
  });
322
327
  const res = await revoke({
323
328
  path: `${KB}/Sales`,
@@ -12,6 +12,7 @@ import { createAccessRoutes } from '../access.routes.js';
12
12
  import type { Database } from '../../database/connection.js';
13
13
  import { usersDbDouble } from './users-db-double.js';
14
14
  import { testKbContext } from '../../../__tests__/kb-context.js';
15
+ import { NodeFs } from '../../kb-fs/node-fs.js';
15
16
 
16
17
  /**
17
18
  * A grant to an email with NO ACCOUNT says so, and stays allowed.
@@ -112,6 +113,7 @@ async function makeHarness(opts: {
112
113
  { emit: vi.fn() } as unknown as WorkflowEventBus,
113
114
  opts.db ?? usersDbDouble(opts.accounts ?? []),
114
115
  testKbContext({ kbDirName: KB }),
116
+ new NodeFs(),
115
117
  ),
116
118
  );
117
119
 
@@ -204,7 +204,7 @@ describe('AccessControlService.grantSources', () => {
204
204
  });
205
205
  });
206
206
 
207
- describe('grantSources — principal kind under group shadowing', () => {
207
+ describe('grantSources — principal kind when a group shares a role name', () => {
208
208
  let root: string;
209
209
  let repo: string;
210
210
  let svc: AccessControlService;
@@ -216,8 +216,8 @@ describe('grantSources — principal kind under group shadowing', () => {
216
216
  repo = path.join(workspaceDir, PROCESS_MAP_DIR);
217
217
  await fs.mkdir(repo, { recursive: true });
218
218
  await writeFile(repo, 'roles.yaml', ROLES_YAML);
219
- // A GROUP named Engineer shadows the Engineer ROLE: the bare token is the
220
- // group's, `role/engineer` is the role's.
219
+ // A GROUP named Engineer shares the Engineer ROLE's name: the bare token
220
+ // and `role/engineer` are the role's, `group/engineer` is the group's.
221
221
  await writeFile(repo, 'groups.yaml', 'groups:\n Engineer:\n - pat@x.io\n');
222
222
  svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR, new NodeFs());
223
223
  });
@@ -226,13 +226,13 @@ describe('grantSources — principal kind under group shadowing', () => {
226
226
  await fs.rm(root, { recursive: true, force: true });
227
227
  });
228
228
 
229
- it("a shadowed BARE grant belongs to the GROUP — it must not hide the role's ancestor grant", async () => {
230
- // The cubic case: bare (group) grant on the target, role/ grant on the
231
- // ancestor. Attributing the bare token to the role would report the role
232
- // as `direct` here — hiding the real role/<Name> ancestor grant and
233
- // making its revoke a false no-op.
229
+ it("a group/ grant belongs to the GROUP — it must not hide the role's ancestor grant", async () => {
230
+ // group/ grant on the target, role/ grant on the ancestor. Attributing the
231
+ // group's token to the role would report the role as `direct` here —
232
+ // hiding the real role/<Name> ancestor grant and making its revoke a
233
+ // false no-op.
234
234
  await writeFile(repo, 'access.md', '---\nwrite:\n - role/Engineer\n---\n');
235
- await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
235
+ await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - group/Engineer\n---\n');
236
236
 
237
237
  const roleSrc = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
238
238
  kind: 'role',
@@ -243,23 +243,28 @@ describe('grantSources — principal kind under group shadowing', () => {
243
243
 
244
244
  const groupSrc = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
245
245
  kind: 'role',
246
- role: 'Engineer', // bare = whoever owns the bare key = the group
246
+ role: 'group/Engineer',
247
247
  });
248
- // The GROUP's only entry is the direct bare grant.
248
+ // The GROUP's only entry is the direct group/ grant.
249
249
  expect(groupSrc.write).toEqual([{ kind: 'direct' }]);
250
250
  });
251
251
 
252
- it("a shadowed role/ grant is never attributed to the GROUP principal", async () => {
252
+ it('a BARE grant of a shared name is the ROLE\'s, never attributed to the GROUP principal', async () => {
253
253
  await writeFile(repo, 'access.md', '---\nwrite:\n - Admin\n---\n');
254
- await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - role/Engineer\n---\n');
254
+ await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
255
255
  const groupSrc = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
256
256
  kind: 'role',
257
- role: 'Engineer',
257
+ role: 'group/Engineer',
258
258
  });
259
259
  expect(groupSrc).toEqual({}); // the group holds nothing here
260
+ const roleSrc = await svc.grantSources(workspaceId, 'folder', 'Knowledge', {
261
+ kind: 'role',
262
+ role: 'role/Engineer',
263
+ });
264
+ expect(roleSrc.write).toEqual([{ kind: 'direct' }]);
260
265
  });
261
266
 
262
- it('UNSHADOWED both spellings still count as the role (legacy bare grants stay removable)', async () => {
267
+ it('with no group of the name, both spellings count as the role (legacy bare grants stay removable)', async () => {
263
268
  await fs.rm(path.join(repo, 'groups.yaml'));
264
269
  await writeFile(repo, 'access.md', '---\nwrite:\n - role/Engineer\n---\n');
265
270
  await writeFile(repo, 'Knowledge/access.md', '---\nwrite:\n - Engineer\n---\n');
@@ -0,0 +1,61 @@
1
+ import type { Server } from 'node:http';
2
+ import type { AddressInfo } from 'node:net';
3
+ import express from 'express';
4
+ import { describe, it, expect, afterEach, vi } from 'vitest';
5
+
6
+ import type { IAdminAccessService } from '../../admin/index.js';
7
+ import type { GroupsAdminService } from '../groups-admin.service.js';
8
+ import { createGroupsAdminRoutes } from '../groups-admin.routes.js';
9
+
10
+ /**
11
+ * GET /admin/groups is a read: the roster is filtered for the email the admin
12
+ * gate checked, and a failing user lookup (needed only to attribute commits)
13
+ * never fails it.
14
+ */
15
+ describe('GET /admin/groups', () => {
16
+ let server: Server | null = null;
17
+
18
+ afterEach(async () => {
19
+ await new Promise<void>((resolve) => (server ? server.close(() => resolve()) : resolve()));
20
+ server = null;
21
+ });
22
+
23
+ async function start(email: string, isAdmin: boolean) {
24
+ const getRoster = vi.fn(async (viewer: string) => ({ viewer, groups: [] }));
25
+ const getUserById = vi.fn(async () => {
26
+ throw new Error('users table unavailable');
27
+ });
28
+ const app = express();
29
+ app.use((req, _res, next) => {
30
+ req.userId = 'u-1';
31
+ req.userEmail = email;
32
+ next();
33
+ });
34
+ app.use(
35
+ createGroupsAdminRoutes({
36
+ groupsAdmin: { getRoster } as unknown as GroupsAdminService,
37
+ adminAccess: { isAdmin: async () => isAdmin } as unknown as IAdminAccessService,
38
+ getUserById,
39
+ }),
40
+ );
41
+ server = app.listen(0);
42
+ await new Promise<void>((resolve) => server!.once('listening', () => resolve()));
43
+ const { port } = server.address() as AddressInfo;
44
+ return { url: `http://127.0.0.1:${port}/admin/groups`, getRoster, getUserById };
45
+ }
46
+
47
+ it('reads the roster for the gated email without a user lookup', async () => {
48
+ const { url, getRoster, getUserById } = await start('admin@x.io', true);
49
+ const res = await fetch(url);
50
+ expect(res.status).toBe(200);
51
+ expect(await res.json()).toEqual({ viewer: 'admin@x.io', groups: [] });
52
+ expect(getRoster).toHaveBeenCalledWith('admin@x.io');
53
+ expect(getUserById).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it('still refuses a non-admin', async () => {
57
+ const { url, getRoster } = await start('pat@x.io', false);
58
+ expect((await fetch(url)).status).toBe(403);
59
+ expect(getRoster).not.toHaveBeenCalled();
60
+ });
61
+ });