@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
@@ -72,7 +72,7 @@ export interface SyncedGroupsSource {
72
72
 
73
73
  // Name safety is the SHARED predicate in `group-files.ts` (`unsafeNameReason`)
74
74
  // — the same rules the manual group editor asserts, including the reserved
75
- // `role/` prefix. Here the name arrives from the IdP, so an unsafe group is
75
+ // `role/` and `group/` prefixes. Here the name arrives from the IdP, so an unsafe group is
76
76
  // SKIPPED with a warning instead of erroring (fail-closed: an unrepresentable
77
77
  // group grants nothing).
78
78
 
@@ -6,8 +6,44 @@ import {
6
6
  accessFrontmatterExtensionList,
7
7
  parseOwnAccessEntries,
8
8
  parseRolesYaml,
9
+ parseAccessEntry,
10
+ mergeGroupsIntoRoles,
9
11
  } from '../access-grammar.js';
10
- import { parseGroupsFile } from '../group-files.js';
12
+ import { parseGroupsFile, unsafeNameReason } from '../group-files.js';
13
+
14
+ describe('principal spellings — a bare name is the role, group/<Name> the group', () => {
15
+ it('parses group/<Name> into the canonical group token, spacing normalised', () => {
16
+ const r = parseAccessEntry('group/ Product Team');
17
+ expect(r).toEqual({ ok: true, entry: { kind: 'role', role: 'group/product team', displayRole: 'group/ Product Team', deny: false } });
18
+ const d = parseAccessEntry('deny group/Admin');
19
+ expect(d.ok && d.entry.kind === 'role' && d.entry.role).toBe('group/admin');
20
+ expect(parseAccessEntry('group/').ok).toBe(false);
21
+ });
22
+
23
+ it('reserves the group/ prefix for roles and groups alike', () => {
24
+ const roles = parseRolesYaml('roles:\n Admin:\n - a@x.io\n group/Ops:\n - b@x.io\n');
25
+ expect(roles.ok).toBe(false);
26
+ if (!roles.ok) expect(roles.errors.join(' ')).toMatch(/reserved 'group\/' prefix/);
27
+ expect(unsafeNameReason('group/Ops')).toMatch(/group\//);
28
+ });
29
+
30
+ it('merging keeps the bare key for the role and registers group/ for every group', () => {
31
+ const parsed = parseRolesYaml('roles:\n Admin:\n - admin@x.io\n');
32
+ if (!parsed.ok) throw new Error('roles');
33
+ const groups = parseGroupsFile('groups:\n Admin:\n - mallory@x.io\n Sales:\n - sam@x.io\n', 'groups.yaml');
34
+ if (!groups.ok) throw new Error('groups');
35
+ const warnings = mergeGroupsIntoRoles(parsed.index, groups.groups, 'groups.yaml');
36
+ const idx = parsed.index.byCanonical;
37
+ expect(idx.get('admin')?.kind).toBe('role');
38
+ expect(idx.get('role/admin')).toBe(idx.get('admin'));
39
+ expect(idx.get('group/admin')?.kind).toBe('group');
40
+ expect(idx.get('sales')?.kind).toBe('group');
41
+ expect(idx.get('group/sales')).toBe(idx.get('sales'));
42
+ // The group's member holds only the group's keys, never the role's.
43
+ expect([...(parsed.index.byEmail.get('mallory@x.io') ?? [])]).toEqual(['group/admin']);
44
+ expect(warnings.join(' ')).toMatch(/resolves to the ROLE; use 'group\/admin'/);
45
+ });
46
+ });
11
47
 
12
48
  describe('parseYamlSubset — inline empty collections', () => {
13
49
  it('reads `key: []` as an empty list, not the scalar "[]"', () => {
@@ -103,6 +139,7 @@ describe('the access-frontmatter extension set', () => {
103
139
  expect(accessFrontmatterExtensionList()).toHaveLength(after);
104
140
  expect(accessFrontmatterExtensionList()).toContain('.md');
105
141
  expect(accessFrontmatterExtensionList()).toContain('.tool');
142
+ expect(accessFrontmatterExtensionList()).not.toContain('.agent');
106
143
  });
107
144
 
108
145
  it('throws on a malformed extension rather than skipping it', () => {
@@ -302,12 +302,13 @@ describe('round-trip — every granted entry parses back', () => {
302
302
  });
303
303
  });
304
304
 
305
- describe('token-kind family — exact-token grants, shadow-aware revokes', () => {
306
- // One name, two spellings, two possible principals: the bare token is the
307
- // GROUP's whenever a group shadows the name (group-first precedence), the
308
- // `role/` token is always the ROLE's. Grants must be exact-token idempotent;
309
- // revokes are alias-tolerant ONLY when unshadowed (the caller passes
310
- // tokenMatch: 'exact' when a group owns the bare name).
305
+ describe('token-kind family — exact-token grants, kind-aware revokes', () => {
306
+ // One name, three spellings, two possible principals: `role/` is always the
307
+ // ROLE's, `group/` always the GROUP's, and the bare token is the role's when
308
+ // one exists, else the group's. Grants must be exact-token idempotent;
309
+ // revokes are alias-tolerant (explicit + bare, never the other kind's
310
+ // explicit spelling) only when the bare name is the principal's — the
311
+ // caller passes tokenMatch: 'exact' otherwise.
311
312
  const SHARED = `---
312
313
  write:
313
314
  - role/Sales
@@ -360,10 +361,51 @@ write:
360
361
  expect(lines).not.toContain(' - Sales');
361
362
  });
362
363
 
363
- it("REVOKE unshadowed bare principal (group since vanished): 'name' matching strips both spellings", () => {
364
+ it("REVOKE bare principal: 'name' matching strips both role spellings", () => {
364
365
  const r = spliceRevoke(SHARED, 'write', { kind: 'role', role: 'Sales' });
365
366
  expect(r.changed).toBe(true);
366
- expect(r.text).not.toContain('Sales'); // bare token falls back to the role — same principal
367
+ expect(r.text).not.toContain('Sales'); // the bare token is the role — same principal
368
+ });
369
+
370
+ const ALL = `---
371
+ write:
372
+ - role/Sales
373
+ - Sales
374
+ - group/Sales
375
+ - Admin
376
+ ---
377
+ `;
378
+
379
+ it('GRANT is exact-token: a group/<Name> grant goes through beside the bare and role/ spellings', () => {
380
+ const md = '---\nwrite:\n - role/Sales\n - Sales\n---\n';
381
+ const r = spliceGrant(md, 'write', { kind: 'role', role: 'group/Sales' });
382
+ expect(r.changed).toBe(true);
383
+ expect(r.text.split('\n')).toContain(' - group/Sales');
384
+ expect(spliceGrant(ALL, 'write', { kind: 'role', role: 'group/Sales' }).changed).toBe(false);
385
+ });
386
+
387
+ it("REVOKE 'name' never crosses kinds: the role's revoke keeps group/, the group's keeps role/", () => {
388
+ const asRole = spliceRevoke(ALL, 'write', { kind: 'role', role: 'role/Sales' });
389
+ expect(asRole.text.split('\n')).toEqual(['---', 'write:', ' - group/Sales', ' - Admin', '---', '']);
390
+
391
+ // The service asks 'name' for a GROUP only while no role shares the name
392
+ // (with one, as in ALL, it picks 'exact' — the last test below), so the
393
+ // group's case is a file where the bare token can only be the group.
394
+ const GROUP_ONLY = '---\nwrite:\n - Sales\n - group/Sales\n - Admin\n---\n';
395
+ const asGroup = spliceRevoke(GROUP_ONLY, 'write', { kind: 'role', role: 'group/Sales' });
396
+ expect(asGroup.text.split('\n')).toEqual(['---', 'write:', ' - Admin', '---', '']);
397
+ });
398
+
399
+ it('REVOKE matches a spaced `group/ <Name>` request against the parsed `group/<name>` entry', () => {
400
+ const r = spliceRevoke(ALL, 'write', { kind: 'role', role: 'group/ Sales' }, { tokenMatch: 'exact' });
401
+ expect(r.text.split('\n')).toEqual(['---', 'write:', ' - role/Sales', ' - Sales', ' - Admin', '---', '']);
402
+ const md = '---\nwrite:\n - group/ Sales\n---\n';
403
+ expect(spliceRevoke(md, 'write', { kind: 'role', role: 'group/Sales' }).changed).toBe(true);
404
+ });
405
+
406
+ it("REVOKE 'exact' of group/<Name> strips only that spelling", () => {
407
+ const r = spliceRevoke(ALL, 'write', { kind: 'role', role: 'group/Sales' }, { tokenMatch: 'exact' });
408
+ expect(r.text.split('\n')).toEqual(['---', 'write:', ' - role/Sales', ' - Sales', ' - Admin', '---', '']);
367
409
  });
368
410
  });
369
411
 
@@ -92,14 +92,47 @@ export const DENY_PREFIX = 'deny ';
92
92
  export const GROUP_REF_PREFIX = 'group:';
93
93
  /**
94
94
  * Explicit ROLE token prefix in access.md entries: `role/<Name>` resolves to
95
- * the roles.yaml role only, never a group. A BARE name resolves GROUP-FIRST
96
- * and falls back to the role — so `role/` is the escape hatch when a group
97
- * shares the role's name. The prefix is reserved in the group name-safety
98
- * rules (a group may never be named `role/...`), and every role is also
99
- * registered in the principal index under its `role/<canonical>` alias.
95
+ * the roles.yaml role only, never a group. A BARE name resolves ROLE-FIRST
96
+ * and falls back to the group, so a group that shares a role's name is
97
+ * reached only as `group/<Name>` (see `GROUP_TOKEN_PREFIX`) — a group named
98
+ * `Admin` never takes over the grants written for the Admin role. The prefix
99
+ * is reserved in the group name-safety rules (a group may never be named
100
+ * `role/...`), and every role is also registered in the principal index under
101
+ * its `role/<canonical>` alias.
100
102
  */
101
103
  export const ROLE_TOKEN_PREFIX = 'role/';
102
104
 
105
+ /**
106
+ * Explicit GROUP token prefix in access.md entries: `group/<Name>` resolves
107
+ * to the active source's group only, never a role. Every group is registered
108
+ * in the principal index under its `group/<canonical>` alias; the bare name
109
+ * reaches the group only while no role of that name exists. The share dialog
110
+ * and the access API always write groups this way. Reserved in the group
111
+ * name-safety rules and in roles.yaml like `role/`.
112
+ */
113
+ export const GROUP_TOKEN_PREFIX = 'group/';
114
+
115
+ /**
116
+ * A canonical role-shaped token split into its explicit kind prefix and its
117
+ * bare name: `role/admin` → `{ prefix: 'role', name: 'admin' }`, `group/admin`
118
+ * → `{ prefix: 'group', name: 'admin' }`, anything else (a bare name, a
119
+ * plugin token, `everyone`) → `{ prefix: null, name: <token> }`. A prefixed
120
+ * name is canonicalised again, so `group/ admin` splits like `group/admin`
121
+ * (as `parseAccessEntry` reads it).
122
+ */
123
+ export function splitPrincipalToken(canonical: string): {
124
+ prefix: 'role' | 'group' | null;
125
+ name: string;
126
+ } {
127
+ if (canonical.startsWith(ROLE_TOKEN_PREFIX)) {
128
+ return { prefix: 'role', name: canonicalRoleName(canonical.slice(ROLE_TOKEN_PREFIX.length)) };
129
+ }
130
+ if (canonical.startsWith(GROUP_TOKEN_PREFIX)) {
131
+ return { prefix: 'group', name: canonicalRoleName(canonical.slice(GROUP_TOKEN_PREFIX.length)) };
132
+ }
133
+ return { prefix: null, name: canonical };
134
+ }
135
+
103
136
  /**
104
137
  * PLUGIN-principal token prefix in access.md entries: `plugin/<Name>/<verb>`
105
138
  * names everyone who holds `<verb>` on the plugin folder `Plugins/<Name>` —
@@ -585,6 +618,11 @@ export function parseAccessEntry(
585
618
  const suffix = canonicalRoleName(role.slice(ROLE_TOKEN_PREFIX.length));
586
619
  if (!suffix) return { ok: false, error: `entry '${body}' names no role after '${ROLE_TOKEN_PREFIX}'` };
587
620
  role = `${ROLE_TOKEN_PREFIX}${suffix}`;
621
+ } else if (role.startsWith(GROUP_TOKEN_PREFIX)) {
622
+ // Explicit group token, normalised the same way to `group/<canonicalName>`.
623
+ const suffix = canonicalRoleName(role.slice(GROUP_TOKEN_PREFIX.length));
624
+ if (!suffix) return { ok: false, error: `entry '${body}' names no group after '${GROUP_TOKEN_PREFIX}'` };
625
+ role = `${GROUP_TOKEN_PREFIX}${suffix}`;
588
626
  } else if (role.startsWith(PLUGIN_TOKEN_PREFIX)) {
589
627
  // `plugin/<Name>/<verb>`: the name is canonicalised to the plugin's
590
628
  // manifest slug, the verb must be one of the three. Anything else is a
@@ -649,6 +687,12 @@ export function parseRolesYaml(
649
687
  );
650
688
  continue;
651
689
  }
690
+ if (canonical.startsWith(GROUP_TOKEN_PREFIX)) {
691
+ errors.push(
692
+ `roles.yaml: role '${displayName}' starts with the reserved '${GROUP_TOKEN_PREFIX}' prefix — that spelling is the explicit group token in access entries`,
693
+ );
694
+ continue;
695
+ }
652
696
  // A role spelled `plugin/…` would be overwritten by the synthesised plugin
653
697
  // principal of the same key while its members kept the token — plugin
654
698
  // access for people who are not plugin members. Refused at parse time,
@@ -730,15 +774,17 @@ export function parseRolesYaml(
730
774
  *
731
775
  * Rules (the grant-grammar precedence):
732
776
  * - Every role is ALSO registered under its explicit `role/<canonical>`
733
- * alias — the token that always resolves to the role.
734
- * - A BARE name resolves GROUP-FIRST: when a group's canonical name
735
- * collides with a role's, the bare key resolves to the GROUP (warned);
736
- * the role stays reachable via `role/<canonical>`.
777
+ * alias, and every group under its explicit `group/<canonical>` alias —
778
+ * the tokens that always resolve to that kind.
779
+ * - A BARE name resolves ROLE-FIRST: when a group's canonical name
780
+ * collides with a role's, the bare key stays the ROLE's (warned); the
781
+ * group is reachable only via `group/<canonical>`. So a group named
782
+ * `Admin` — whoever proposed it — never gains what `Admin` is granted.
737
783
  * - Role `group:<Name>` refs — Admin's included — expand against the
738
784
  * merged groups; an unknown ref contributes nothing (warned).
739
785
  * - `byEmail` is rebuilt so each member holds exactly the tokens that
740
786
  * resolve to a principal they belong to (bare + `role/` alias for roles,
741
- * bare for groups).
787
+ * `group/` alias plus the bare name when the group owns it for groups).
742
788
  */
743
789
  export function mergeGroupsIntoRoles(
744
790
  index: RolesIndex,
@@ -765,19 +811,18 @@ export function mergeGroupsIntoRoles(
765
811
  }
766
812
  }
767
813
 
768
- // 2. Bare-name precedence: groups win the bare token; the role keeps its
769
- // `role/<canonical>` alias registered below.
814
+ // 2. Bare-name precedence: roles keep the bare token; every group gets its
815
+ // `group/<canonical>` alias, and the bare key only when no role owns it.
770
816
  for (const [canonical, def] of groups) {
817
+ const group = { displayName: def.displayName, emails: new Set(def.emails), kind: 'group' as const };
818
+ index.byCanonical.set(`${GROUP_TOKEN_PREFIX}${canonical}`, group);
771
819
  if (roleRecords.has(canonical)) {
772
820
  warnings.push(
773
- `${sourceFile}: group '${def.displayName}' shares its name with a role — the bare name now resolves to the GROUP; use '${ROLE_TOKEN_PREFIX}${canonical}' to reference the role`,
821
+ `${sourceFile}: group '${def.displayName}' shares its name with a role — the bare name resolves to the ROLE; use '${GROUP_TOKEN_PREFIX}${canonical}' to reference the group`,
774
822
  );
823
+ continue;
775
824
  }
776
- index.byCanonical.set(canonical, {
777
- displayName: def.displayName,
778
- emails: new Set(def.emails),
779
- kind: 'group',
780
- });
825
+ index.byCanonical.set(canonical, group);
781
826
  }
782
827
 
783
828
  // 3. Explicit `role/<canonical>` alias for every role (same record — the
@@ -788,8 +833,8 @@ export function mergeGroupsIntoRoles(
788
833
  }
789
834
 
790
835
  // 4. Rebuild the email → tokens map from the final index so membership
791
- // reflects the post-precedence keys (a collided role's members no longer
792
- // hold the bare token unless the group also contains them).
836
+ // reflects the post-precedence keys (a collided group's members hold
837
+ // only its `group/` alias, never the role's bare token).
793
838
  index.byEmail.clear();
794
839
  for (const [key, principal] of index.byCanonical) {
795
840
  for (const email of principal.emails) {
@@ -33,6 +33,7 @@ import {
33
33
  canonicalEmail,
34
34
  canonicalRoleName,
35
35
  accessMdDeclaresBodyRules,
36
+ splitPrincipalToken,
36
37
  } from './access-grammar.js';
37
38
  import { freshAccessMd } from './access-template.js';
38
39
  import { scanFrontmatter } from './frontmatter-lines.js';
@@ -149,15 +150,18 @@ export function validatePrincipal(p: Principal): Principal {
149
150
  /**
150
151
  * How a role-shaped principal's TOKEN is compared against a file entry:
151
152
  *
152
- * - `'exact'`: the entry must carry the SAME spelling — a bare token and a
153
- * `role/<name>` token are DIFFERENT principals (bare = whoever owns the
154
- * bare key under group-first precedence, `role/` = the role). Grants
155
- * always use this: granting the group `X` while `role/X` is present (or
156
- * vice versa) must NOT be treated as already-granted, or the other
157
- * principal silently never receives the grant.
158
- * - `'name'`: both spellings of the name match. Revokes use this ONLY when
159
- * no group shadows the bare name — then both spellings resolve to the
160
- * role, and revoking the role must also remove legacy bare spellings.
153
+ * - `'exact'`: the entry must carry the SAME spelling — a bare token, a
154
+ * `role/<name>` token and a `group/<name>` token can be DIFFERENT
155
+ * principals (bare = the role when one exists, else the group). Grants
156
+ * always use this: granting `group/X` while `role/X` or a bare `X` is
157
+ * present must NOT be treated as already-granted, or the group silently
158
+ * never receives the grant.
159
+ * - `'name'`: the principal's explicit spelling AND the bare name match —
160
+ * `role/X` (or a bare `X`) matches `X` and `role/X`; `group/X` matches
161
+ * `X` and `group/X`. Never across kinds: a role's revoke never touches
162
+ * `group/X`, a group's never `role/X`. Revokes use this only when the
163
+ * bare name is this principal's (AccessMutationService decides), so
164
+ * revoking also removes the legacy bare spelling.
161
165
  */
162
166
  export type TokenMatch = 'exact' | 'name';
163
167
 
@@ -169,11 +173,14 @@ export type TokenMatch = 'exact' | 'name';
169
173
  function entryMatches(entry: ParsedEntry, p: Principal, tokenMatch: TokenMatch): boolean {
170
174
  if (p.kind === 'user') return entry.kind === 'user' && entry.email === canonicalEmail(p.email);
171
175
  if (entry.kind !== 'role') return false;
172
- const canonicalPrincipal = canonicalRoleName(p.role);
173
- if (tokenMatch === 'exact') return entry.role === canonicalPrincipal;
174
- const strip = (token: string): string =>
175
- token.startsWith('role/') ? canonicalRoleName(token.slice('role/'.length)) : token;
176
- return strip(entry.role) === strip(canonicalPrincipal);
176
+ // Compare split, canonical halves: `group/ Product` in the request names the
177
+ // same principal as the `group/product` the parser reads from the file.
178
+ const own = splitPrincipalToken(canonicalRoleName(p.role));
179
+ const theirs = splitPrincipalToken(entry.role);
180
+ if (theirs.name !== own.name) return false;
181
+ if (tokenMatch === 'exact') return theirs.prefix === own.prefix;
182
+ // Same family only: a bare principal stands for the role, as `role/` does.
183
+ return theirs.prefix === null || theirs.prefix === (own.prefix ?? 'role');
177
184
  }
178
185
 
179
186
  // ---------------------------------------------------------------------------
@@ -344,10 +351,10 @@ function blockEntries(fm: string[], block: VerbBlock): { entry: ParsedEntry; lin
344
351
  * Add `principal` under `verb`. Idempotent: if an entry with the EXACT same
345
352
  * token spelling already exists with the SAME deny-ness (a grant when
346
353
  * granting, a deny when denying), returns `changed: false`. Idempotency is
347
- * deliberately exact-token, never name-level: a bare token and a `role/`
348
- * token are different principals (group-first precedence gives the bare key
349
- * to a same-named group), so a bare-`X` GROUP grant must go through even when
350
- * `role/X` is present, and vice versa. Creates the verb key (and the
354
+ * deliberately exact-token, never name-level: a bare token, a `role/` token
355
+ * and a `group/` token can be different principals (the bare key is the
356
+ * role's when one exists), so a `group/X` grant must go through even when a
357
+ * bare `X` or `role/X` is present, and vice versa. Creates the verb key (and the
351
358
  * frontmatter fence, for a fresh file) as needed. Preserves everything else
352
359
  * byte-for-byte.
353
360
  *
@@ -445,11 +452,12 @@ export function spliceGrant(
445
452
  * Preserves everything else.
446
453
  *
447
454
  * Token matching for role-shaped principals is `tokenMatch` (default `'name'`:
448
- * BOTH spellings — revoking the role removes legacy bare spellings too).
449
- * Callers pass `'exact'` when a GROUP shadows the principal's bare name: the
450
- * bare token then belongs to the group and the `role/` token to the role, so a
451
- * revoke of one must never strip the other (see AccessMutationService, which
452
- * derives the flag from the merged principal index).
455
+ * the explicit spelling and the bare one — revoking removes legacy bare
456
+ * spellings too). Callers pass `'exact'` when the bare name belongs to the
457
+ * OTHER kind (a group revoke while a same-named role owns the bare key, or a
458
+ * role revoke while only a group of that name exists), so a revoke of one
459
+ * never strips the other (see AccessMutationService, which derives the flag
460
+ * from the merged principal index).
453
461
  */
454
462
  export function spliceRevoke(
455
463
  text: string,
@@ -4,6 +4,7 @@ import {
4
4
  RESERVED_ROLE_NAMES,
5
5
  PLUGIN_TOKEN_PREFIX,
6
6
  ROLE_TOKEN_PREFIX,
7
+ GROUP_TOKEN_PREFIX,
7
8
  canonicalEmail,
8
9
  canonicalRoleName,
9
10
  parseYamlSubset,
@@ -58,6 +59,7 @@ export const SYNCED_GROUPS_YAML = 'synced-groups.yaml';
58
59
  * \x00-\x1f control chars / newlines break line structure outright
59
60
  * `role/…` reserved: that spelling is the EXPLICIT role token in access
60
61
  * entries — a group carrying it could never be referenced.
62
+ * `group/…` reserved likewise: the EXPLICIT group token.
61
63
  */
62
64
  export function unsafeNameReason(displayName: string): string | null {
63
65
  const trimmed = displayName.trim();
@@ -69,6 +71,9 @@ export function unsafeNameReason(displayName: string): string | null {
69
71
  if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
70
72
  return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
71
73
  }
74
+ if (canonicalRoleName(trimmed).startsWith(GROUP_TOKEN_PREFIX)) {
75
+ return `starts with the reserved '${GROUP_TOKEN_PREFIX}' prefix (the explicit group token in access entries)`;
76
+ }
72
77
  if (canonicalRoleName(trimmed).startsWith(PLUGIN_TOKEN_PREFIX)) {
73
78
  return `starts with the reserved '${PLUGIN_TOKEN_PREFIX}' prefix (the plugin-principal token in access entries)`;
74
79
  }
@@ -367,6 +367,7 @@ describe('the HTML views section', () => {
367
367
  expect(text).toContain('**No host navigation.**');
368
368
  expect(text).toContain('`window.bevel.openNode(href)`');
369
369
  expect(text).toContain('`window.bevel.navigate(href)`');
370
+ expect(text).toContain('`window.bevel.branch` — the branch the page is viewed on');
370
371
  expect(text).toContain('A distribution may add data members of its own to `window.bevel`');
371
372
  expect(text).toContain('**A bare fragment scrolls the page.**');
372
373
  // Rendered for the deployment's layout like every other section.
@@ -374,6 +375,36 @@ describe('the HTML views section', () => {
374
375
  expect(docs).toContain('/workspace/main/Docs/Alice.md');
375
376
  });
376
377
 
378
+ // A dashboard that painted one "Loading…" line and then swapped in the whole
379
+ // board read as broken on a cold load (2026-10-20). These rules are what
380
+ // agents write pages from; the reference page under them is run by
381
+ // core-frontend's `liveDataReferencePage.test.ts`.
382
+ it('tells page authors how a page loads live data without changing shape', async () => {
383
+ const text = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '## HTML views').replace(/\s+/g, ' ');
384
+ expect(text).toContain('### Live data: a page that stays still while it loads');
385
+ expect(text).toContain('**Render the final page shell in the markup, before any data.**');
386
+ expect(text).toContain('The loading treatment — placeholder rows, `aria-busy="true"` — sits **inside** the data regions.');
387
+ expect(text).toContain('**Publish one consistent dataset at a time.**');
388
+ expect(text).toContain('Until then show no counts, no cards and no empty columns');
389
+ expect(text).toContain('**Refresh keeps the current board.**');
390
+ expect(text).toContain('**A failed read fails the whole load, visibly.**');
391
+ expect(text).toContain('offer a Retry button that runs the same load again');
392
+ expect(text).toContain('**Read in bounded parallel once the paths are known.**');
393
+ expect(text).toContain('**Load with `callTool`, not `callToolChain`.**');
394
+ expect(text).toContain("**The reads are the viewer's own.**");
395
+ expect(text).toContain('**Link to what the tools named by the path they answered.**');
396
+ expect(text).toContain('Never put the checkout folder in front of a path a tool answered: it is already there.');
397
+ expect(text).toContain('plain HTML and one inline script, no build step and no framework');
398
+ // The rule names the deployment's own checkout folder; the page links on its branch by the tool's path.
399
+ const docs = section(await composeAgentGuide(DEFAULT_KB_LAYOUT, undefined, { kbDirName: 'kb' }), '## HTML views');
400
+ expect(docs.replace(/\s+/g, ' ')).toContain('starts with the checkout folder, `kb/Data/Tickets/A.md`');
401
+ // Each segment of the tool's path encoded on its own, so a name carrying `#` stays a name.
402
+ expect(docs).toContain(
403
+ "link.href = '/workspace/' + encodeURIComponent(BRANCH) + '/' + ticket.path.split('/').map(encodeURIComponent).join('/');",
404
+ );
405
+ expect(docs).not.toContain('{{');
406
+ });
407
+
377
408
  it('documents window.bevel.tools: its four functions, the null check, the consent rule and requireTools with its card', async () => {
378
409
  const text = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '## HTML views').replace(/\s+/g, ' ');
379
410
  for (const member of [
@@ -102,10 +102,21 @@ export function registerAgentGuideTool(
102
102
  router.post(
103
103
  `/agent/tools/${GET_AGENT_GUIDE_TOOL}`,
104
104
  toolAuth,
105
- toolHandler(async (args) => {
105
+ toolHandler(async (args, ctx) => {
106
106
  const list = await sections();
107
107
  const wanted = typeof args.section === 'string' ? args.section.trim() : '';
108
- if (!wanted) return { guide: joinGuideSections(list), sections: list.map((s) => ({ id: s.id, title: s.title })) };
108
+ // A connection made as an agent configuration is told so at the top of the
109
+ // whole guide: the catalog it describes is narrowed to that agent.
110
+ // Everything is told nothing new, so its guide is the same text.
111
+ const connection = ctx.connectionScope?.describe() ?? null;
112
+ if (!wanted) {
113
+ const guide = joinGuideSections(list);
114
+ return {
115
+ guide: connection ? `> ${connection}\n\n${guide}` : guide,
116
+ sections: list.map((s) => ({ id: s.id, title: s.title })),
117
+ ...(connection ? { connection } : {}),
118
+ };
119
+ }
109
120
  const one = list.find((s) => s.id === wanted);
110
121
  if (!one) {
111
122
  throw new ToolError(
@@ -51,6 +51,7 @@ describe('the shared file rules are one text, in two places', () => {
51
51
  expect(rules.map((r) => r.id)).toEqual([
52
52
  'agent-guide',
53
53
  'content-kinds',
54
+ 'workspace-tmp',
54
55
  'write-mode',
55
56
  'images-in-pages',
56
57
  'escape-sequences',
@@ -90,8 +90,10 @@ export function platformInstructions(layout: KbLayout): string {
90
90
  * own cap, and the preamble under its cap plus the marker a cut appends. The
91
91
  * test is what makes it a ceiling rather than a hope: a shared rule that grew
92
92
  * past it fails before it reaches an agent.
93
+ *
94
+ * 14,400 since the shared rules' own cap rose by 400 (the `__tmp__/` ref).
93
95
  */
94
- export const INSTRUCTIONS_CAP = 14_000;
96
+ export const INSTRUCTIONS_CAP = 14_400;
95
97
 
96
98
  /** The one-line marker that replaces everything past the preamble cap. */
97
99
  export const PREAMBLE_TRUNCATION_MARKER = `[preamble truncated at ${PREAMBLE_CAP.toLocaleString('en-US')} characters; shorten ${PREAMBLE_FILE}]`;
@@ -61,8 +61,12 @@ const PRE_RENAME_AGENTS_FILE = 'CLAUDE.md';
61
61
  * of every call rather than of how to write one, and why large, escape-heavy
62
62
  * and binary content goes by upload, which three write tools each said in
63
63
  * full.
64
+ *
65
+ * 7,400 since the `__tmp__/` ref: four tools take it, so what it reaches and
66
+ * that it is read-only is said once here rather than in each description, and
67
+ * the section had no room left for it.
64
68
  */
65
- export const SHARED_FILE_RULES_CAP = 7_000;
69
+ export const SHARED_FILE_RULES_CAP = 7_400;
66
70
 
67
71
  /** One shared rule: how the guide heads it, and the rule itself. */
68
72
  export interface SharedFileRule {
@@ -114,6 +118,18 @@ export function sharedFileRules(layout: KbLayout): readonly SharedFileRule[] {
114
118
  '`{ image_omitted, note }` stub instead, so read an image outside a chain when you mean to look at it. Any other ' +
115
119
  'binary file reads as a one-line description rather than raw bytes.',
116
120
  },
121
+ {
122
+ // What `__tmp__/` reaches and what it does not (see
123
+ // `workspace/workspace-tmp.ts`) — four tools take the ref, so it is a
124
+ // shared rule rather than a sentence in each description.
125
+ id: 'workspace-tmp',
126
+ heading: "Files staged in the workspace's tmp folder",
127
+ body:
128
+ "`__tmp__/<path>` names ONE file YOU staged in the branch workspace's `tmp/` folder (chat attachments land " +
129
+ 'there; nobody else\'s staged files are reachable). read_file, file_stat, grep (`path`) and copy_file (`src`) ' +
130
+ 'read it as a repository file; it is read-only and never listed — copy_file it to a repository path to keep ' +
131
+ 'it. An entry stays up to seven days.',
132
+ },
117
133
  {
118
134
  id: 'write-mode',
119
135
  heading: 'What a write may do at a path',