@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
@@ -40,11 +40,10 @@ import type { IAccessControl } from './access-control.interface.js';
40
40
  import { isAbsence } from '../../shared/fs.contract.js';
41
41
  import { isTextBytes } from '../workspace/index.js';
42
42
  import {
43
- type Verb,
44
43
  ADMIN_CANONICAL,
45
44
  KNOWN_VERBS,
46
- ROLE_TOKEN_PREFIX,
47
45
  canonicalRoleName,
46
+ splitPrincipalToken,
48
47
  accessFrontmatterExtensionList,
49
48
  } from '../access-model/index.js';
50
49
  import {
@@ -52,9 +51,8 @@ import {
52
51
  spliceGrant,
53
52
  validatePrincipal,
54
53
  AccessSpliceError,
55
- type Principal,
56
- type TokenMatch,
57
54
  } from '../access-model/index.js';
55
+ import type { Verb, Principal, TokenMatch } from '../access-model/index.js';
58
56
  import { WorkflowDomainError } from '../../shared/domain-errors.js';
59
57
  import {
60
58
  canCarryFrontmatter,
@@ -90,23 +88,21 @@ export const ADMIN_ROOT_WRITE_MESSAGE =
90
88
  * this target touches the Admin ROLE's write at the repository root — the
91
89
  * root folder's `access.md`, or a file directly in the root. The resolver
92
90
  * ignores such a rule anyway (the Admin write floor), so writing it would only
93
- * leave a line that says something the app does not do. A bare token matched
94
- * `exact` is a group's (or a shadowed name's), never the Admin role. Targets
95
- * arrive in one spelling: the routes refuse `.` and `..` segments.
91
+ * leave a line that says something the app does not do. A bare `Admin` is the
92
+ * role (a bare name is always the role when one exists, and roles.yaml always
93
+ * declares Admin); `group/Admin` is a group and never the role. Targets arrive
94
+ * in one spelling: the routes refuse `.` and `..` segments.
96
95
  */
97
96
  export function removesAdminRootWrite(
98
97
  kind: TargetKind,
99
98
  repoRelTarget: string,
100
99
  principal: Principal,
101
100
  verb: Verb | undefined,
102
- tokenMatch: TokenMatch,
103
101
  ): boolean {
104
102
  if (principal.kind !== 'role') return false;
105
103
  if (!touchesRootWrite(kind, repoRelTarget, verb)) return false;
106
- const canonical = canonicalRoleName(principal.role);
107
- const explicit = canonical.startsWith(ROLE_TOKEN_PREFIX);
108
- if (!explicit && tokenMatch === 'exact') return false;
109
- return (explicit ? canonical.slice(ROLE_TOKEN_PREFIX.length) : canonical) === ADMIN_CANONICAL;
104
+ const { prefix, name } = splitPrincipalToken(canonicalRoleName(principal.role));
105
+ return prefix !== 'group' && name === ADMIN_CANONICAL;
110
106
  }
111
107
 
112
108
  /** Whether a mutation on `verb` (absent = every verb) at this target reaches write at the repository root. */
@@ -276,38 +272,40 @@ export class AccessMutationService {
276
272
  }
277
273
 
278
274
  /**
279
- * How a REVOKE of `principal` matches file tokens — the shadowing rule:
275
+ * How a REVOKE of `principal` matches file tokens. `'name'` strips the
276
+ * principal's explicit spelling AND the bare name (never the other kind's
277
+ * explicit spelling — see `TokenMatch`); `'exact'` strips the literal
278
+ * spelling alone. The bare name is stripped only when it is THIS
279
+ * principal's under role-first precedence:
280
280
  *
281
- * - When a GROUP owns the principal's bare name (group-first precedence),
282
- * the two spellings are DIFFERENT principals: the bare token is the
283
- * group's, `role/<name>` is the role's. Matching is `'exact'` so
284
- * revoking the role never strips the group's bare grant (and revoking
285
- * the group never strips the role's explicit grant).
286
- * - Unshadowed, both spellings resolve to the ROLE (bare falls back to
287
- * it), so matching is `'name'`: revoking the role also removes legacy
288
- * bare spellings — the historical cleanup behavior.
281
+ * - A ROLE (`role/<name>`, or a bare name) owns the bare key unless no
282
+ * role of that name exists and a group does — then the bare token is
283
+ * the group's, and matching is `'exact'`. A model that fails to load
284
+ * keeps it `'exact'` too.
285
+ * - A GROUP (`group/<name>`) owns the bare key only while no role of
286
+ * that name exists — otherwise the bare token is the role's, and
287
+ * matching is `'exact'`. A group that has VANISHED from the active
288
+ * source still reads correctly: the question is whether a ROLE holds
289
+ * the name. A model that fails to load says nothing about roles, so a
290
+ * group revoke then stays `'exact'` rather than risk a role's grant.
289
291
  *
290
292
  * Judged against THIS workspace's merged principal index (`kbPrincipals` —
291
293
  * the same model the resolver reads), so revoke agrees with resolution on
292
- * who owns the bare key. A model that fails to load yields no groups, i.e.
293
- * unshadowed — matching degrades to the pre-groups name-level behavior.
294
- *
295
- * GROUP revokes never take the name-level path: the route passes
296
- * `tokenMatch: 'exact'` explicitly (see `revoke`'s `opts`), because this
297
- * shadow probe cannot tell a group apart from a role once the group has
298
- * VANISHED from the active source — the bare name then reads "unshadowed"
299
- * and a name-level group revoke would strip a same-named role's live
300
- * `role/<Name>` grant.
294
+ * who owns the bare key.
301
295
  */
302
296
  private async revokeTokenMatch(workspaceId: string, principal: Principal): Promise<TokenMatch> {
303
297
  if (principal.kind !== 'role') return 'exact'; // user matching ignores the mode
304
- const canonical = canonicalRoleName(principal.role);
305
- const bare = canonical.startsWith(ROLE_TOKEN_PREFIX)
306
- ? canonical.slice(ROLE_TOKEN_PREFIX.length)
307
- : canonical;
308
- const { groups } = await this.accessControl.kbPrincipals(workspaceId);
309
- const shadowed = groups.some((g) => canonicalRoleName(g) === bare);
310
- return shadowed ? 'exact' : 'name';
298
+ const { prefix, name } = splitPrincipalToken(canonicalRoleName(principal.role));
299
+ const { roles, groups } = await this.accessControl.kbPrincipals(workspaceId);
300
+ const isRole = roles.some((r) => canonicalRoleName(r) === name);
301
+ if (prefix === 'group') {
302
+ // `roles` always carries at least `Everyone` once the model loads.
303
+ return isRole || roles.length === 0 ? 'exact' : 'name';
304
+ }
305
+ const isGroup = groups.some((g) => canonicalRoleName(g) === name);
306
+ // No roles at all means the model failed to load: stay exact, so a role
307
+ // revoke never strips a same-named group's bare grant.
308
+ return roles.length === 0 || (!isRole && isGroup) ? 'exact' : 'name';
311
309
  }
312
310
 
313
311
  /**
@@ -372,11 +370,10 @@ export class AccessMutationService {
372
370
  // When present, strip ONLY this verb (a per-checkbox toggle in the share UI);
373
371
  // absent strips the principal from every verb (the whole-principal Remove).
374
372
  verb?: Verb,
375
- // `tokenMatch` overrides the shadow-derived matching. The route passes
376
- // 'exact' for GROUP principals: a group's grant is its bare token only,
377
- // and once the group has vanished the shadow probe can no longer tell it
378
- // from a role — name-level matching would then strip a same-named role's
379
- // `role/<Name>` grant.
373
+ // `tokenMatch` overrides the derived matching (see revokeTokenMatch).
374
+ // One production caller: the plugin-links unlink pins `'exact'` for its
375
+ // `plugin/<Name>/<verb>` tokens. Pin only `'exact'` — a pinned `'name'`
376
+ // would bypass the guard that never strips across kinds.
380
377
  opts?: { tokenMatch?: TokenMatch },
381
378
  ): Promise<{ changed: boolean; editPath: string }> {
382
379
  void _actingUserEmail; // referenced to satisfy no-unused-vars until something consumes it
@@ -386,8 +383,8 @@ export class AccessMutationService {
386
383
  // missing FILE node is a bad target and should surface, not silently no-op
387
384
  // — same rule grant() uses.
388
385
  const original = await this.readOrEmpty(workspaceId, kind, repoRelTarget, editPath);
389
- // Alias-tolerant vs exact-token matching, decided by group shadowing —
390
- // see revokeTokenMatch — unless the caller pinned it.
386
+ // Alias-tolerant vs exact-token matching, decided by who owns the bare
387
+ // name — see revokeTokenMatch — unless the caller pinned it.
391
388
  const tokenMatch = opts?.tokenMatch ?? (await this.revokeTokenMatch(workspaceId, principal));
392
389
  let next = original;
393
390
  let changed = false;
@@ -409,7 +406,7 @@ export class AccessMutationService {
409
406
  // Refused only when an Admin write entry would really go: a whole-row
410
407
  // Remove at a root that grants Admin other verbs but no write line (write
411
408
  // comes from the floor) strips those verbs as usual.
412
- if (writeRemoved && removesAdminRootWrite(kind, repoRelTarget, principal, verb, tokenMatch)) {
409
+ if (writeRemoved && removesAdminRootWrite(kind, repoRelTarget, principal, verb)) {
413
410
  throw adminRootWriteRefusal();
414
411
  }
415
412
  if (!changed) return { changed: false, editPath };
@@ -457,7 +454,7 @@ export class AccessMutationService {
457
454
  repoRelTarget: string,
458
455
  principal: Principal,
459
456
  verb?: Verb,
460
- // Same override as `revoke` — the route pins 'exact' for GROUP principals.
457
+ // Same override as `revoke`; no production caller passes it here.
461
458
  opts?: { tokenMatch?: TokenMatch },
462
459
  ): Promise<{ changed: boolean; editPath: string }> {
463
460
  this.assertPrincipalSafe(principal);
@@ -465,10 +462,10 @@ export class AccessMutationService {
465
462
  const original = await this.readOrEmpty(workspaceId, kind, repoRelTarget, editPath);
466
463
 
467
464
  const verbsToDeny = verb ? [verb] : KNOWN_VERBS;
468
- // Same shadowing-aware matching as revoke(): the strip must not swallow a
469
- // same-named OTHER principal's grant (bare = group vs role/<name> = role).
465
+ // Same matching as revoke(): the strip must not swallow a same-named
466
+ // OTHER principal's grant (`group/<name>` vs the role's bare/`role/`).
470
467
  const tokenMatch = opts?.tokenMatch ?? (await this.revokeTokenMatch(workspaceId, principal));
471
- if (removesAdminRootWrite(kind, repoRelTarget, principal, verb, tokenMatch)) {
468
+ if (removesAdminRootWrite(kind, repoRelTarget, principal, verb)) {
472
469
  throw adminRootWriteRefusal();
473
470
  }
474
471
  // A PERSON who is an admin keeps root write through the floor, so a deny
@@ -508,10 +505,8 @@ export class AccessMutationService {
508
505
 
509
506
  // (3) Assert the deny actually removed effective access on the targeted
510
507
  // verb(s) — else roll back. The check evaluates the SAME token identity
511
- // the deny above spliced (`tokenMatch` rides along): with a pinned exact
512
- // GROUP deny whose group has vanished, alias-tolerant matching would read
513
- // a same-named role's surviving `role/<Name>` grant as the group still
514
- // having access — rolling back a fully effective deny as "ineffective".
508
+ // the deny above spliced (`tokenMatch` rides along), so a pinned exact
509
+ // deny is never judged by spellings it did not touch.
515
510
  const stillHas = await this.principalStillHasAccess(
516
511
  workspaceId,
517
512
  kind,
@@ -13,6 +13,7 @@ import {
13
13
  EVERYONE_CANONICAL,
14
14
  KNOWN_VERBS,
15
15
  ROLE_TOKEN_PREFIX,
16
+ GROUP_TOKEN_PREFIX,
16
17
  type Verb,
17
18
  } from '../access-model/index.js';
18
19
 
@@ -51,13 +52,12 @@ export async function resolveAccessView(
51
52
  // files). Keyed `u:<email>` / `r:<role>` / `g:<group>` to match the
52
53
  // dialog's row keys, so each row can show where its access comes from and
53
54
  // which verbs are removable here. Groups get their OWN `g:` namespace: a
54
- // group and a role sharing a name are DIFFERENT principals (bare token vs
55
- // `role/<name>`), and one shared `r:` entry could only describe one of
56
- // them. Each kind resolves through the token spelling that IS that
57
- // principal — a group through its bare token (group-first precedence), a
58
- // role through its explicit `role/<name>` alias (correct whether or not a
59
- // group shadows the name; the built-in `everyone` keeps its bare spelling,
60
- // it has no alias). A row whose verbs resolve only via a
55
+ // group and a role sharing a name are DIFFERENT principals (`group/<name>`
56
+ // vs the bare name and `role/<name>`), and one shared `r:` entry could only
57
+ // describe one of them. Each kind resolves through the explicit token that
58
+ // IS that principal — a group through `group/<name>`, a role through
59
+ // `role/<name>` (the built-in `everyone` keeps its bare spelling, it has no
60
+ // alias); the bare spelling counts toward whichever kind owns it. A row whose verbs resolve only via a
61
61
  // group/everyone/rescue has no source (the verb is absent) and renders
62
62
  // non-actionable. Built over the union of every principal in the four
63
63
  // eligible lists (kinded `principals`, with the name-only `roles` list as
@@ -96,9 +96,11 @@ export async function resolveAccessView(
96
96
  await Promise.all([
97
97
  ...[...collectives.entries()].map(async ([key, p]) => {
98
98
  const token =
99
- p.kind === 'role' && canonicalRoleName(p.name) !== EVERYONE_CANONICAL
100
- ? `${ROLE_TOKEN_PREFIX}${p.name}`
101
- : p.name;
99
+ p.kind === 'group'
100
+ ? `${GROUP_TOKEN_PREFIX}${p.name}`
101
+ : p.kind === 'role' && canonicalRoleName(p.name) !== EVERYONE_CANONICAL
102
+ ? `${ROLE_TOKEN_PREFIX}${p.name}`
103
+ : p.name;
102
104
  const principal = { kind: 'role' as const, role: token };
103
105
  const [grants, denied] = await Promise.all([
104
106
  accessControl.grantSources(workspaceId, kind, repoRelTarget, principal),
@@ -19,6 +19,7 @@ import type {
19
19
  } from './access-control.interface.js';
20
20
  import type { WorkspaceService } from '../workspace/index.js';
21
21
  import type { KbContext } from '../../shared/kb-context.js';
22
+ import type { ITreeWalker } from '../../shared/fs.contract.js';
22
23
  import { branchForWorkspaceId } from '../../shared/workspace-id.js';
23
24
  import type { AuthService, IAccountSwitchGuard } from '../auth/index.js';
24
25
  import type { WorkflowService, WorkflowEventBus } from '../workflow/index.js';
@@ -39,7 +40,10 @@ import {
39
40
  PLUGIN_TOKEN_PREFIX,
40
41
  PLUGIN_TOKEN_VERBS,
41
42
  ROLE_TOKEN_PREFIX,
43
+ GROUP_TOKEN_PREFIX,
44
+ splitPrincipalToken,
42
45
  type PluginTokenVerb,
46
+ type Principal,
43
47
  type Verb,
44
48
  } from '../access-model/index.js';
45
49
  import { listAccessDeclarationsUnder } from './access-declarations.js';
@@ -47,7 +51,6 @@ import { emailsInView, holderPrincipals, labelAccountHolders, resolveAccessView
47
51
  import type { AccessView, LabelledAccessView } from './access-view.js';
48
52
  import { toHttpError as sharedToHttpError, requireNonEmptyString as sharedRequireNonEmptyString } from './admin-route-helpers.js';
49
53
  import { RolesAdminService } from './roles-admin.service.js';
50
- import type { Principal } from '../access-model/index.js';
51
54
  import type { Database } from '../database/index.js';
52
55
  import { users } from '../database/index.js';
53
56
  import { canonicalEmail } from '../../shared/email-identity.js';
@@ -91,6 +94,8 @@ export function createAccessRoutes(
91
94
  eventBus: WorkflowEventBus,
92
95
  db: Database,
93
96
  kb: KbContext,
97
+ /** The composition root's walk, for the roles admin's reference scan. */
98
+ disk: ITreeWalker,
94
99
  /** This deployment's configured admins — the break-glass recovery roster. */
95
100
  recoveryAdmins: readonly string[] = [],
96
101
  ): express.Router {
@@ -101,6 +106,7 @@ export function createAccessRoutes(
101
106
  workspaceService,
102
107
  workflowService,
103
108
  accessControl,
109
+ disk,
104
110
  kbDirName,
105
111
  () => kb.defaultBranch,
106
112
  eventBus,
@@ -161,10 +167,10 @@ export function createAccessRoutes(
161
167
  /**
162
168
  * What the mutation routes accept as a principal. `group` and `plugin`
163
169
  * exist only at this boundary: in the access.md entry grammar a group grant
164
- * is a bare-name token (bare names resolve GROUP-FIRST, then fall back to
165
- * the role) and a plugin grant is the `plugin/<Name>/<verb>` token, so both
166
- * are spliced as role-shaped principals — the separate kinds buy validation
167
- * against the right namespace and honest 404s.
170
+ * is the `group/<Name>` token and a plugin grant is the
171
+ * `plugin/<Name>/<verb>` token, so both are spliced as role-shaped
172
+ * principals — the separate kinds buy validation against the right
173
+ * namespace and honest 404s.
168
174
  */
169
175
  type RoutePrincipal =
170
176
  | Principal
@@ -173,14 +179,16 @@ export function createAccessRoutes(
173
179
 
174
180
  /**
175
181
  * The role-shaped principal the splice layer actually writes/matches.
176
- * - group → its bare name (group-first precedence resolves it).
177
- * - role → the explicit `role/<Name>` token — the grant route WRITES
178
- * roles that way from now on (bare role tokens remain
179
- * read-compatible). The built-in `everyone` stays bare: it
180
- * is not a roles.yaml role and has no `role/` alias.
182
+ * - group → the explicit `group/<Name>` token. Never the bare name:
183
+ * a bare name is the ROLE whenever one of that name exists,
184
+ * so a bare group grant could silently mean someone else.
185
+ * - role → the explicit `role/<Name>` token. Bare spellings in
186
+ * existing files remain read-compatible. The built-in
187
+ * `everyone` stays bare: it is not a roles.yaml role and
188
+ * has no `role/` alias.
181
189
  */
182
190
  const asSplicePrincipal = (p: RoutePrincipal): Principal => {
183
- if (p.kind === 'group') return { kind: 'role', role: p.group };
191
+ if (p.kind === 'group') return { kind: 'role', role: `${GROUP_TOKEN_PREFIX}${groupName(p.group)}` };
184
192
  // Written with the folder's own casing; `parseAccessEntry` canonicalises
185
193
  // the name half to the manifest slug on the way back in.
186
194
  if (p.kind === 'plugin') return { kind: 'role', role: `${PLUGIN_TOKEN_PREFIX}${p.plugin.trim()}/${p.verb}` };
@@ -209,6 +217,14 @@ export function createAccessRoutes(
209
217
  return p;
210
218
  };
211
219
 
220
+ /** A group as the caller named it, without a `group/` prefix it may carry (casing kept). */
221
+ function groupName(group: string): string {
222
+ const raw = group.trim();
223
+ return splitPrincipalToken(canonicalRoleName(raw)).prefix === 'group'
224
+ ? raw.slice(GROUP_TOKEN_PREFIX.length).trim()
225
+ : raw;
226
+ }
227
+
212
228
  // Authentication gate only. Per PLAN §3 the workspace `:id` is a branch
213
229
  // identifier — any authenticated user can read its access tree.
214
230
  async function requireUser(
@@ -511,8 +527,8 @@ export function createAccessRoutes(
511
527
  * signed in — so the dialog can label them; it withholds nobody.
512
528
  *
513
529
  * A name shared by a group and a role is offered as BOTH — nothing is
514
- * withheld: grant precedence resolves the collision (bare token = the
515
- * group; the role is written as `role/<Name>`).
530
+ * withheld: each is written with its own prefix (`group/<Name>`,
531
+ * `role/<Name>`), so the collision never reaches the file.
516
532
  */
517
533
  router.get('/workspace/:id/access/suggest', async (req, res) => {
518
534
  const user = await requireUser(req, res);
@@ -948,10 +964,10 @@ export function createAccessRoutes(
948
964
  // uses — and one the caller may see (`visibleGroups`): a group hidden
949
965
  // from the picker is refused here with the same words as one that
950
966
  // does not exist, so the grant route is no oracle for group names.
951
- // No role-collision refusal: bare tokens resolve GROUP-FIRST, so the
952
- // written bare name IS the group.
967
+ // No role-collision refusal: the grant is written `group/<Name>`,
968
+ // which always resolves to the group.
953
969
  const groups = await visibleGroups(user.email, (await defaultBranchPrincipals()).groups);
954
- const canonical = canonicalRoleName(principal.group);
970
+ const canonical = canonicalRoleName(groupName(principal.group));
955
971
  if (!groups.some((g) => canonicalRoleName(g) === canonical)) {
956
972
  throw new AccessMutationError(
957
973
  `No group named "${principal.group}". Pick an existing group.`,
@@ -976,8 +992,8 @@ export function createAccessRoutes(
976
992
  );
977
993
  }
978
994
  }
979
- // Splice shape: group → bare token (group-first precedence); role →
980
- // explicit `role/<Name>` token (see asSplicePrincipal).
995
+ // Splice shape: group → explicit `group/<Name>` token; role → explicit
996
+ // `role/<Name>` token (see asSplicePrincipal).
981
997
  const splicePrincipal = asSplicePrincipal(principal);
982
998
 
983
999
  const { gatePath } = gateAndEditPaths(kind, repoRelTarget);
@@ -1045,18 +1061,12 @@ export function createAccessRoutes(
1045
1061
  try {
1046
1062
  const mode = (req.body as { mode?: unknown }).mode;
1047
1063
  const { repoRelTarget, kind, principal: routePrincipal } = parseMutationBody(req.body);
1048
- // A group principal converts straight to the role-shaped bare token —
1049
- // no existence check (revoking a grant whose group has since vanished
1050
- // must keep working).
1064
+ // A group principal converts straight to its `group/<Name>` token — no
1065
+ // existence check (revoking a grant whose group has since vanished must
1066
+ // keep working). Which spellings a revoke strips is the mutation
1067
+ // service's call: `group/<Name>` never matches `role/<Name>`, and the
1068
+ // bare name only when no role of that name owns it.
1051
1069
  const principal = asSplicePrincipal(routePrincipal);
1052
- // GROUP revokes match the EXACT bare token only. The service's own
1053
- // shadow probe decides exact-vs-name for roles, but it cannot know a
1054
- // bare token was a GROUP once that group vanished from the active
1055
- // source — the name would read "unshadowed" and name-level matching
1056
- // would strip a same-named role's live `role/<Name>` grant. The route
1057
- // still knows the caller's kind, so it pins the matching here.
1058
- const revokeOpts =
1059
- routePrincipal.kind === 'group' ? ({ tokenMatch: 'exact' } as const) : undefined;
1060
1070
  // Optional `verb` scopes ALL three paths to a single verb: a default revoke
1061
1071
  // strips just that verb; remove-from-parent removes just that verb on the
1062
1072
  // ancestor; deny-here denies just that verb at the target. Absent ⇒ the
@@ -1112,7 +1122,7 @@ export function createAccessRoutes(
1112
1122
  );
1113
1123
  }
1114
1124
  // Scope the ancestor revoke to the same verb the user acted on (if any).
1115
- await mutation.revoke(workspaceId, 'folder', ancestorDir, principal, user.email, verb, revokeOpts);
1125
+ await mutation.revoke(workspaceId, 'folder', ancestorDir, principal, user.email, verb);
1116
1126
  });
1117
1127
 
1118
1128
  accessControl.invalidate(workspaceId);
@@ -1131,7 +1141,7 @@ export function createAccessRoutes(
1131
1141
  await mutation.assertTargetHoldsText(workspaceId, kind, repoRelTarget);
1132
1142
  await withEditLock(workspaceId, branch, gatePath, user, async () => {
1133
1143
  // Scope the deny to the same verb the user acted on (if any).
1134
- await mutation.denyHere(workspaceId, kind, repoRelTarget, principal, verb, revokeOpts);
1144
+ await mutation.denyHere(workspaceId, kind, repoRelTarget, principal, verb);
1135
1145
  });
1136
1146
  accessControl.invalidate(workspaceId);
1137
1147
  logger('access.revoke.deny-here').info(
@@ -1150,7 +1160,7 @@ export function createAccessRoutes(
1150
1160
  const editPath = gatePath;
1151
1161
  let changed = false;
1152
1162
  await withEditLock(workspaceId, branch, editPath, user, async () => {
1153
- const r = await mutation.revoke(workspaceId, kind, repoRelTarget, principal, user.email, verb, revokeOpts);
1163
+ const r = await mutation.revoke(workspaceId, kind, repoRelTarget, principal, user.email, verb);
1154
1164
  changed = r.changed;
1155
1165
  });
1156
1166
 
@@ -1247,7 +1257,7 @@ export function createAccessRoutes(
1247
1257
  // surface). The frontend hides the page, but the API must gate too — the
1248
1258
  // roster exposes every role + member email.
1249
1259
  await assertRolesAdmin(user.email);
1250
- res.json({ roles: await rolesAdmin.getRoster() });
1260
+ res.json({ roles: await rolesAdmin.getRoster(user.email) });
1251
1261
  } catch (err) {
1252
1262
  const { status, body } = toHttpError(err);
1253
1263
  res.status(status).json(body);
@@ -50,9 +50,11 @@ export function createGroupsAdminRoutes(deps: {
50
50
  // role assignments) + `groupsHealth` (the broken-source banner marker; a
51
51
  // broken MANUAL groups.yaml instead answers 422 with the parse message so
52
52
  // the operator sees what to fix).
53
- router.get('/admin/groups', requireAdmin, async (_req, res) => {
53
+ router.get('/admin/groups', requireAdmin, async (req, res) => {
54
54
  try {
55
- res.json(await groupsAdmin.getRoster());
55
+ // A read needs no commit attribution: the roster is filtered for the
56
+ // email `requireAdmin` just checked, with no second user lookup to fail.
57
+ res.json(await groupsAdmin.getRoster(req.userEmail!));
56
58
  } catch (err) {
57
59
  sendError(res, err);
58
60
  }