@bevel-software/platform-core-backend 0.4.1 → 0.5.1

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 (305) hide show
  1. package/THIRD-PARTY-NOTICES.md +6118 -0
  2. package/dist/core/core-ports.d.ts +15 -0
  3. package/dist/core/core-ports.d.ts.map +1 -1
  4. package/dist/core/core-ports.js.map +1 -1
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +40 -6
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +17 -1
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +153 -29
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core-config.d.ts +70 -21
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +134 -58
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +6 -0
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +74 -5
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +116 -15
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access-declarations.d.ts +93 -0
  23. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  24. package/dist/modules/access/access-declarations.js +168 -0
  25. package/dist/modules/access/access-declarations.js.map +1 -0
  26. package/dist/modules/access/access-mutation.service.js +4 -4
  27. package/dist/modules/access/access-mutation.service.js.map +1 -1
  28. package/dist/modules/access/access-splice.d.ts +16 -1
  29. package/dist/modules/access/access-splice.d.ts.map +1 -1
  30. package/dist/modules/access/access-splice.js +40 -21
  31. package/dist/modules/access/access-splice.js.map +1 -1
  32. package/dist/modules/access/access.routes.d.ts.map +1 -1
  33. package/dist/modules/access/access.routes.js +51 -0
  34. package/dist/modules/access/access.routes.js.map +1 -1
  35. package/dist/modules/access/creator-access.d.ts +8 -0
  36. package/dist/modules/access/creator-access.d.ts.map +1 -1
  37. package/dist/modules/access/creator-access.js +21 -2
  38. package/dist/modules/access/creator-access.js.map +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  40. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  41. package/dist/modules/admin/admin-access.service.js +3 -2
  42. package/dist/modules/admin/admin-access.service.js.map +1 -1
  43. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  44. package/dist/modules/auth/auth.routes.js +50 -1
  45. package/dist/modules/auth/auth.routes.js.map +1 -1
  46. package/dist/modules/auth/auth.service.d.ts +39 -10
  47. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  48. package/dist/modules/auth/auth.service.js +38 -16
  49. package/dist/modules/auth/auth.service.js.map +1 -1
  50. package/dist/modules/database/core-schema.d.ts +124 -0
  51. package/dist/modules/database/core-schema.d.ts.map +1 -1
  52. package/dist/modules/database/core-schema.js +45 -0
  53. package/dist/modules/database/core-schema.js.map +1 -1
  54. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  55. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  56. package/dist/modules/groups/group-provision.service.js +288 -0
  57. package/dist/modules/groups/group-provision.service.js.map +1 -0
  58. package/dist/modules/groups/groups.contract.d.ts +106 -0
  59. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  60. package/dist/modules/groups/groups.contract.js +36 -0
  61. package/dist/modules/groups/groups.contract.js.map +1 -0
  62. package/dist/modules/groups/groups.routes.d.ts +42 -0
  63. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  64. package/dist/modules/groups/groups.routes.js +379 -0
  65. package/dist/modules/groups/groups.routes.js.map +1 -0
  66. package/dist/modules/groups/groups.service.d.ts +60 -0
  67. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  68. package/dist/modules/groups/groups.service.js +172 -0
  69. package/dist/modules/groups/groups.service.js.map +1 -0
  70. package/dist/modules/groups/index.d.ts +7 -0
  71. package/dist/modules/groups/index.d.ts.map +1 -0
  72. package/dist/modules/groups/index.js +6 -0
  73. package/dist/modules/groups/index.js.map +1 -0
  74. package/dist/modules/groups/join-proposals.d.ts +53 -0
  75. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  76. package/dist/modules/groups/join-proposals.js +67 -0
  77. package/dist/modules/groups/join-proposals.js.map +1 -0
  78. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  79. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  80. package/dist/modules/groups/join-requests.service.js +135 -0
  81. package/dist/modules/groups/join-requests.service.js.map +1 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  83. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  85. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  86. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  87. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  88. package/dist/modules/settings/deployment-settings.service.js +368 -0
  89. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  90. package/dist/modules/settings/setup.routes.d.ts +49 -0
  91. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  92. package/dist/modules/settings/setup.routes.js +292 -0
  93. package/dist/modules/settings/setup.routes.js.map +1 -0
  94. package/dist/modules/skills/index.d.ts +2 -1
  95. package/dist/modules/skills/index.d.ts.map +1 -1
  96. package/dist/modules/skills/index.js +1 -0
  97. package/dist/modules/skills/index.js.map +1 -1
  98. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  99. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  100. package/dist/modules/skills/pending-skills.service.js +147 -0
  101. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  102. package/dist/modules/skills/skills.contract.d.ts +32 -3
  103. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  104. package/dist/modules/skills/skills.contract.js +1 -1
  105. package/dist/modules/skills/skills.routes.d.ts +3 -2
  106. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  107. package/dist/modules/skills/skills.routes.js +18 -1
  108. package/dist/modules/skills/skills.routes.js.map +1 -1
  109. package/dist/modules/skills/skills.service.d.ts +22 -0
  110. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  111. package/dist/modules/skills/skills.service.js +24 -12
  112. package/dist/modules/skills/skills.service.js.map +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts +1 -1
  114. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  115. package/dist/modules/tool-manuals/index.js.map +1 -1
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  117. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  120. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  122. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  125. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  126. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  129. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  132. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  133. package/dist/modules/workflow/event-bus.d.ts +11 -0
  134. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  135. package/dist/modules/workflow/event-bus.js +22 -0
  136. package/dist/modules/workflow/event-bus.js.map +1 -1
  137. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  138. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  139. package/dist/modules/workflow/file-lock.service.js +16 -0
  140. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  142. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  143. package/dist/modules/workflow/git/branch-name.js +1 -1
  144. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  145. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  146. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  147. package/dist/modules/workflow/git/git.service.js +144 -52
  148. package/dist/modules/workflow/git/git.service.js.map +1 -1
  149. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  150. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  151. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  152. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  155. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  157. package/dist/modules/workflow/workflow.routes.js +95 -14
  158. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  159. package/dist/modules/workflow/workflow.service.d.ts +92 -3
  160. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  161. package/dist/modules/workflow/workflow.service.js +381 -108
  162. package/dist/modules/workflow/workflow.service.js.map +1 -1
  163. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  164. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  165. package/dist/modules/workspace/bevel-ignore.js +5 -1
  166. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  167. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  168. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  169. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  170. package/dist/modules/workspace/kb-seed.service.js +228 -51
  171. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  173. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  174. package/dist/modules/workspace/workspace.routes.js +51 -18
  175. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  176. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  177. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  178. package/dist/modules/workspace/workspace.service.js +36 -9
  179. package/dist/modules/workspace/workspace.service.js.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  181. package/dist/modules/workspace/workspace.tools.js +8 -3
  182. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  183. package/dist/shared/kb-layout.test.js +2 -2
  184. package/dist/test-setup.d.ts +2 -0
  185. package/dist/test-setup.d.ts.map +1 -0
  186. package/dist/test-setup.js +18 -0
  187. package/dist/test-setup.js.map +1 -0
  188. package/kb-template/.bevelignore +6 -2
  189. package/kb-template/AGENTS.md +272 -0
  190. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  191. package/kb-template/access.md +36 -36
  192. package/migrations/0002_user_onboarding_done.sql +1 -0
  193. package/migrations/0003_deployment_settings.sql +9 -0
  194. package/migrations/meta/0001_snapshot.json +1486 -0
  195. package/migrations/meta/0002_snapshot.json +1493 -0
  196. package/migrations/meta/0003_snapshot.json +1552 -0
  197. package/migrations/meta/_journal.json +15 -1
  198. package/package.json +5 -4
  199. package/src/__tests__/branch-model.test.ts +79 -0
  200. package/src/__tests__/core-config.admin.test.ts +114 -0
  201. package/src/__tests__/database-url.test.ts +70 -0
  202. package/src/core/core-ports.ts +15 -0
  203. package/src/core/create-core-server.ts +55 -6
  204. package/src/core/create-core-services.ts +201 -36
  205. package/src/core-config.ts +139 -59
  206. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  207. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  208. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  209. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  210. package/src/modules/access/access-control.interface.ts +11 -0
  211. package/src/modules/access/access-control.service.ts +113 -15
  212. package/src/modules/access/access-declarations.ts +251 -0
  213. package/src/modules/access/access-mutation.service.ts +4 -4
  214. package/src/modules/access/access-splice.ts +64 -21
  215. package/src/modules/access/access.routes.ts +61 -0
  216. package/src/modules/access/creator-access.ts +22 -2
  217. package/src/modules/admin/admin-access.service.ts +4 -3
  218. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  219. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  220. package/src/modules/auth/auth.routes.ts +50 -1
  221. package/src/modules/auth/auth.service.ts +55 -17
  222. package/src/modules/database/core-schema.ts +46 -0
  223. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  224. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  225. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  226. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  227. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  228. package/src/modules/groups/group-provision.service.ts +328 -0
  229. package/src/modules/groups/groups.contract.ts +107 -0
  230. package/src/modules/groups/groups.routes.ts +433 -0
  231. package/src/modules/groups/groups.service.ts +193 -0
  232. package/src/modules/groups/index.ts +17 -0
  233. package/src/modules/groups/join-proposals.ts +114 -0
  234. package/src/modules/groups/join-requests.service.ts +185 -0
  235. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  236. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  237. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  238. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  239. package/src/modules/settings/deployment-settings.service.ts +419 -0
  240. package/src/modules/settings/setup.routes.ts +317 -0
  241. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  242. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  243. package/src/modules/skills/index.ts +3 -0
  244. package/src/modules/skills/pending-skills.service.ts +168 -0
  245. package/src/modules/skills/skills.contract.ts +34 -3
  246. package/src/modules/skills/skills.routes.ts +23 -2
  247. package/src/modules/skills/skills.service.ts +24 -16
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  251. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  252. package/src/modules/tool-manuals/index.ts +2 -0
  253. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  254. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  255. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  256. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  257. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +251 -5
  259. package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
  260. package/src/modules/workflow/event-bus.ts +25 -0
  261. package/src/modules/workflow/file-lock.service.ts +17 -0
  262. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
  263. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
  264. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
  265. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
  266. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
  267. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
  268. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +5 -0
  269. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
  270. package/src/modules/workflow/git/branch-name.ts +5 -1
  271. package/src/modules/workflow/git/git.service.ts +159 -62
  272. package/src/modules/workflow/git/pull-request.service.ts +22 -11
  273. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
  274. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  275. package/src/modules/workflow/workflow.routes.ts +91 -13
  276. package/src/modules/workflow/workflow.service.ts +353 -13
  277. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
  278. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
  279. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
  280. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
  281. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
  283. package/src/modules/workspace/bevel-ignore.ts +66 -62
  284. package/src/modules/workspace/kb-seed.interface.ts +1 -1
  285. package/src/modules/workspace/kb-seed.service.ts +247 -53
  286. package/src/modules/workspace/workspace.routes.ts +54 -18
  287. package/src/modules/workspace/workspace.service.ts +45 -7
  288. package/src/modules/workspace/workspace.tools.ts +922 -917
  289. package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
  290. package/src/shared/__tests__/join-request.test.ts +96 -0
  291. package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
  292. package/src/shared/kb-layout.test.ts +2 -2
  293. package/src/test-setup.ts +18 -0
  294. package/kb-template/Agents/README.md +0 -38
  295. package/kb-template/CLAUDE.md +0 -419
  296. package/kb-template/Data/.gitkeep +0 -0
  297. package/kb-template/Data/README.md +0 -39
  298. package/kb-template/KnowledgeBase/README.md +0 -37
  299. package/kb-template/Pipelines/.gitkeep +0 -0
  300. package/kb-template/Pipelines/README.md +0 -38
  301. package/kb-template/Skills/.gitkeep +0 -0
  302. package/kb-template/Skills/README.md +0 -32
  303. package/kb-template/Tools/.gitkeep +0 -0
  304. package/kb-template/Tools/README.md +0 -42
  305. /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
@@ -84,6 +84,10 @@ export function createToolManualsAgentRoutes(
84
84
  * Browser-facing tool-manual routes (mounted under the JWT auth middleware):
85
85
  * GET /tools — the caller's accessible `.tool` manuals (summaries).
86
86
  * POST /tools/preview — validate a draft `.tool` for the renderer.
87
+ * GET /tools/:slug — one readable manual with description + capabilities
88
+ * (the tool page). Registered LAST so no future
89
+ * literal sibling is shadowed by the param segment;
90
+ * `preview` is POST-only, so it never collides.
87
91
  */
88
92
  export function createToolManualsBrowserRoutes(toolManualService: IToolManualService): express.Router {
89
93
  const router = express.Router();
@@ -106,5 +110,22 @@ export function createToolManualsBrowserRoutes(toolManualService: IToolManualSer
106
110
  res.json(await toolManualService.preview(content));
107
111
  });
108
112
 
113
+ router.get('/tools/:slug', async (req, res) => {
114
+ const email = req.userEmail;
115
+ if (!email) return void res.status(401).json({ error: 'Not authenticated' });
116
+ try {
117
+ // `getDetail` returns null for BOTH "no such slug" and "you can't read it",
118
+ // and this route keeps them indistinguishable: a distinct 403 would confirm
119
+ // the existence of a tool the caller isn't allowed to know about. Same
120
+ // fail-closed posture as the agent-facing `/tools/:slug/manual`.
121
+ const tool = await toolManualService.getDetail(email, String(req.params.slug));
122
+ if (!tool) return void res.status(404).json({ error: 'Not found' });
123
+ res.json({ tool });
124
+ } catch (err) {
125
+ console.error('[tool-manuals] detail failed:', err);
126
+ res.status(500).json({ error: 'Failed to load tool' });
127
+ }
128
+ });
129
+
109
130
  return router;
110
131
  }
@@ -9,7 +9,7 @@ import {
9
9
  DefaultVariableSubstitutor,
10
10
  type CallTemplate,
11
11
  } from '@utcp/sdk';
12
- import { DEFAULT_BRANCH, TOOLS_DIR } from '@bevel-software/platform-shared';
12
+ import { DEFAULT_BRANCH, GROUPS_DIR } from '@bevel-software/platform-shared';
13
13
  import type { WorkspaceService } from '../workspace/workspace.service.js';
14
14
  import { workspaceIdForBranch } from '../workspace/workspace.service.js';
15
15
  import type { IAccessControl } from '../access/access-control.interface.js';
@@ -28,6 +28,8 @@ import {
28
28
  type IToolManualService,
29
29
  type ToolManualDescriptor,
30
30
  type ToolManualSummary,
31
+ type ToolManualDetail,
32
+ type ToolCapability,
31
33
  type UtcpManualDict,
32
34
  type ToolManualPreview,
33
35
  type ToolManualType,
@@ -38,6 +40,13 @@ import {
38
40
 
39
41
  const CACHE_TTL_MS = 60_000;
40
42
 
43
+ /**
44
+ * How many embedded tools a manual may advertise on the tool page. A `.tool` is
45
+ * author-written, so its `tools` array is unbounded input; the page renders one
46
+ * bullet per entry, and a thousand of them is a broken page, not a useful one.
47
+ */
48
+ const MAX_CAPABILITIES = 100;
49
+
41
50
  /**
42
51
  * UTCP namespaces a user `.tool` may NOT claim: they belong to built-in manuals
43
52
  * whose loopback creds are pre-seeded on the agent's code-mode client (the
@@ -154,16 +163,25 @@ export class ToolManualService implements IToolManualService {
154
163
  }
155
164
 
156
165
  async listAccessible(userEmail: string): Promise<ToolManualSummary[]> {
157
- const manuals = await this.accessibleManuals(userEmail);
158
- return manuals.map((m) => ({
159
- slug: m.slug,
160
- name: m.name,
161
- path: m.path,
162
- type: m.type,
163
- variables: m.variables,
164
- remote: m.remote,
165
- setup: m.setup,
166
- }));
166
+ return (await this.accessibleManuals(userEmail)).map(toSummary);
167
+ }
168
+
169
+ async listAllSummaries(): Promise<ToolManualSummary[]> {
170
+ return (await this.scan()).map(toSummary);
171
+ }
172
+
173
+ async getDetail(userEmail: string, slug: string): Promise<ToolManualDetail | null> {
174
+ // Resolved through `accessibleManuals` rather than `scan()` + a canRead call
175
+ // so the cache, the dedupe, the mcp-oauth decoration and the fail-closed
176
+ // batch ACL all apply exactly as they do to the catalog listing — one read
177
+ // model, no second place for the access rules to drift.
178
+ const found = (await this.accessibleManuals(userEmail)).find((m) => m.slug === slug);
179
+ if (!found) return null;
180
+ return {
181
+ ...toSummary(found),
182
+ description: found.description ?? null,
183
+ capabilities: capabilitiesOf(found),
184
+ };
167
185
  }
168
186
 
169
187
  async listLocalOnly(userEmail: string): Promise<{ name: string; path: string }[]> {
@@ -252,7 +270,7 @@ export class ToolManualService implements IToolManualService {
252
270
  async preview(content: string): Promise<ToolManualPreview> {
253
271
  let descriptor: ToolManualDescriptor;
254
272
  try {
255
- descriptor = normalizeToolManual('draft', 'Tools/draft.tool', content);
273
+ descriptor = normalizeToolManual('draft', 'Groups/draft.tool', content);
256
274
  } catch (err) {
257
275
  return { ok: false, errors: [err instanceof Error ? err.message : String(err)] };
258
276
  }
@@ -467,12 +485,14 @@ export class ToolManualService implements IToolManualService {
467
485
  } catch {
468
486
  return [];
469
487
  }
470
- const root = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName, TOOLS_DIR);
488
+ const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
471
489
 
472
- const files = (await walkFiles(root, (n) => n.toLowerCase().endsWith('.tool'))).map((rel) => ({
473
- abs: path.join(root, rel),
474
- rel: `${TOOLS_DIR}/${rel}`,
475
- }));
490
+ // A `.tool` sits under `Groups/`, beside the skills that use it.
491
+ const files: { abs: string; rel: string }[] = [];
492
+ const root = path.join(kbRoot, GROUPS_DIR);
493
+ for (const rel of await walkFiles(root, (n) => n.toLowerCase().endsWith('.tool'))) {
494
+ files.push({ abs: path.join(root, rel), rel: `${GROUPS_DIR}/${rel}` });
495
+ }
476
496
 
477
497
  const parsed: ToolManualDescriptor[] = [];
478
498
  for (const f of files) {
@@ -510,6 +530,47 @@ export class ToolManualService implements IToolManualService {
510
530
 
511
531
  // --- helpers ------------------------------------------------------------------
512
532
 
533
+ /**
534
+ * The one descriptor → summary projection, shared by every list surface
535
+ * (`listAccessible`, `listAllSummaries`, and the summary half of `getDetail`).
536
+ */
537
+ function toSummary(m: ToolManualDescriptor): ToolManualSummary {
538
+ return {
539
+ slug: m.slug,
540
+ name: m.name,
541
+ path: m.path,
542
+ type: m.type,
543
+ description: m.description,
544
+ variables: m.variables,
545
+ remote: m.remote,
546
+ setup: m.setup,
547
+ };
548
+ }
549
+
550
+ /**
551
+ * What an `inline` manual's embedded tools say the assistant can do. `tools` is
552
+ * typed `unknown[]` (it is only validated when actually served as a UTCP
553
+ * manual), so every entry is re-checked here: an entry without a string `name`
554
+ * has nothing to display and is dropped rather than rendering a blank bullet.
555
+ * Non-inline manuals discover their tools over the network at call time, so
556
+ * there is nothing to derive without a round-trip this endpoint won't make.
557
+ */
558
+ function capabilitiesOf(m: ToolManualDescriptor): ToolCapability[] {
559
+ if (m.type !== 'inline' || !Array.isArray(m.tools)) return [];
560
+ const out: ToolCapability[] = [];
561
+ for (const entry of m.tools) {
562
+ if (out.length >= MAX_CAPABILITIES) break;
563
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
564
+ const t = entry as Record<string, unknown>;
565
+ if (typeof t.name !== 'string' || !t.name.trim()) continue;
566
+ out.push({
567
+ name: t.name.trim(),
568
+ description: typeof t.description === 'string' && t.description.trim() ? t.description.trim() : null,
569
+ });
570
+ }
571
+ return out;
572
+ }
573
+
513
574
  function baseName(rel: string): string {
514
575
  const base = rel.slice(rel.lastIndexOf('/') + 1);
515
576
  return base.replace(/\.tool$/i, '');
@@ -600,6 +661,14 @@ export function normalizeToolManual(
600
661
  type,
601
662
  };
602
663
 
664
+ // Cosmetic prose for the browser tool page. Unlike every other field here, a
665
+ // malformed value is IGNORED rather than thrown on: `description` buys the
666
+ // reader a sentence, and no sentence is worth taking a working integration out
667
+ // of the catalog (a throw skips the whole file). So: a non-empty string wins,
668
+ // anything else — number, object, null, blank — silently leaves it absent.
669
+ const description = typeof obj.description === 'string' ? obj.description.trim() : '';
670
+ if (description) descriptor.description = description;
671
+
603
672
  const variables = normalizeVariables(obj.variables);
604
673
  if (variables.length) descriptor.variables = variables;
605
674
 
@@ -1,202 +1,206 @@
1
- import type { Router, RequestHandler } from 'express';
2
- import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
3
- import type { IToolRegistry, UtcpTool } from '../tool-registry/tool.contract.js';
4
- import type { ToolContext } from '../tool-helpers/tool.contract.js';
5
- import { toolDef } from '../tool-helpers/tool-def.js';
6
- import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
7
- import type { IAccessControl } from '../access/access-control.interface.js';
8
- import { workspaceIdForBranch } from '../workspace/workspace.service.js';
9
- import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
10
- import type { IToolManualService } from './tool-manuals.contract.js';
11
-
12
- /**
13
- * What `list_tool_setup` needs from the secrets vault (structurally satisfied
14
- * by `ISecretsVaultService.statusFor`). A local port keeps this module free of
15
- * a secrets-vault import — the same decoupling discipline as
16
- * `McpAuthDiscoveryPort` in the service.
17
- */
18
- export interface VariableStatusPort {
19
- statusFor(
20
- userId: string,
21
- keys: string[],
22
- ): Promise<{ key: string; adminConfigured: boolean; userConfigured: boolean; userAuthorized?: boolean }[]>;
23
- }
24
-
25
- /**
26
- * Registers the tool-manual catalog tools (both surfaces) and hosts their
27
- * endpoints:
28
- *
29
- * - `list_local_tools` — local-only manuals (`remote: false`) the remote proxy
30
- * can't serve; the agent reads their `.tool` path and self-configures.
31
- * - `list_tool_setup` — configuration status of every accessible manual, so an
32
- * agent can EXPLAIN the remaining setup to the admin. Deliberately
33
- * read-only: secret VALUES are never set (or returned) through a tool — the
34
- * admin pastes them into the tool editor; users sign in on /connect.
35
- */
36
- export function registerToolManualsTools(
37
- registry: IToolRegistry,
38
- router: Router,
39
- toolAuth: RequestHandler,
40
- toolHandler: ToolHandlerFactory,
41
- toolManualService: IToolManualService,
42
- deps: {
43
- accessControl: IAccessControl;
44
- variableStatus: VariableStatusPort;
45
- },
46
- ): void {
47
- registry.registerExternalTool((ctx) => buildListLocalToolsDef(toolManualService, ctx.userEmail));
48
- registry.registerInternalTool((ctx) => buildListLocalToolsDef(toolManualService, ctx.userEmail));
49
-
50
- router.post(
51
- '/agent/tools/list_local_tools',
52
- toolAuth,
53
- toolHandler(async (_args, ctx: ToolContext) => ({
54
- tools: await toolManualService.listLocalOnly(ctx.user.email),
55
- })),
56
- );
57
-
58
- const defaultWs = workspaceIdForBranch(DEFAULT_BRANCH);
59
- const varKey = (manualName: string, varName: string) => utcpNamespacedKey(manualName, varName);
60
-
61
- const listSetupDef = toolDef({
62
- name: 'list_tool_setup',
63
- description:
64
- 'Configuration status of every `.tool` the current user can access: what each tool needs set up and ' +
65
- 'what is already configured. Results are scoped to the caller — a `.tool` the caller cannot READ is ' +
66
- 'absent entirely, and all status flags reflect the caller\'s own state. Per tool: `setup` describes ' +
67
- 'an MCP server\'s sign-in requirement (`open` = none; `oauth-auto` = sign-in was configured ' +
68
- 'automatically; `oauth-manual` = the provider does not support automatic registration, so a writer ' +
69
- 'must declare the OAuth provider in the `.tool` file and paste its client secret into the tool ' +
70
- 'editor). Per variable: whether the shared (admin) value is set, whether the CURRENT user has ' +
71
- 'set/authorized their own, and whether it is an OAuth sign-in (users authorize those on the /connect ' +
72
- 'page, never by typing a value). `canWrite` = the caller may write THAT `.tool` FILE (per-file access ' +
73
- 'from its frontmatter `write:`/`owner:` verbs and the access.md chain — NOT a platform role), which ' +
74
- 'is exactly what gates setting its shared secrets: the people who manage the file configure the tool. ' +
75
- 'Secret VALUES are never returned and can never be set through a tool — an admin enters them in the ' +
76
- 'tool editor; users sign in on /connect.',
77
- path: '/api/agent/tools/list_tool_setup',
78
- inputs: { type: 'object', properties: {}, required: [], additionalProperties: false },
79
- outputs: {
80
- type: 'object',
81
- properties: {
82
- tools: {
83
- type: 'array',
84
- items: {
85
- type: 'object',
86
- properties: {
87
- slug: { type: 'string' },
88
- name: { type: 'string', description: 'The manual id — the namespace secrets bind to.' },
89
- path: { type: 'string' },
90
- type: { type: 'string' },
91
- setup: {
92
- type: ['object', 'null'],
93
- description: 'MCP auto-discovery setup requirement; null for non-mcp tools.',
94
- properties: { kind: { type: 'string' }, reason: { type: 'string' } },
95
- },
96
- canWrite: { type: 'boolean' },
97
- variables: {
98
- type: 'array',
99
- items: {
100
- type: 'object',
101
- properties: {
102
- name: { type: 'string' },
103
- scope: { type: 'string', description: '`admin` (one shared value) or `user` (per user).' },
104
- label: { type: ['string', 'null'] },
105
- oauth: { type: 'boolean', description: 'Filled by signing in on /connect, not by a value.' },
106
- adminConfigured: {
107
- type: 'boolean',
108
- description:
109
- 'Shared value set (plain admin vars), or the owner finished provider setup (OAuth vars).',
110
- },
111
- userConfigured: { type: 'boolean' },
112
- authorized: { type: ['boolean', 'null'], description: 'OAuth vars: current user signed in.' },
113
- },
114
- required: ['name', 'scope', 'oauth', 'adminConfigured', 'userConfigured'],
115
- },
116
- },
117
- },
118
- required: ['slug', 'name', 'path', 'type', 'canWrite', 'variables'],
119
- },
120
- },
121
- },
122
- required: ['tools'],
123
- },
124
- tags: ['tools'],
125
- });
126
- registry.registerInternalTool(listSetupDef);
127
- registry.registerExternalTool(listSetupDef);
128
- router.post(
129
- '/agent/tools/list_tool_setup',
130
- toolAuth,
131
- toolHandler(async (_args, ctx: ToolContext) => {
132
- const manuals = await toolManualService.listAccessible(ctx.user.email);
133
- const allKeys = manuals.flatMap((m) => (m.variables ?? []).map((v) => varKey(m.name, v.name)));
134
- const status = await deps.variableStatus.statusFor(ctx.user.id, allKeys);
135
- const statusByKey = new Map(status.map((s) => [s.key, s]));
136
- const tools = await Promise.all(
137
- manuals.map(async (m) => ({
138
- slug: m.slug,
139
- name: m.name,
140
- path: m.path,
141
- type: m.type,
142
- setup: m.setup ?? null,
143
- canWrite: await deps.accessControl.canWrite(defaultWs, ctx.user.email, m.path),
144
- variables: (m.variables ?? []).map((v) => {
145
- const st = statusByKey.get(varKey(m.name, v.name));
146
- const isOAuth = v.oauth != null;
147
- return {
148
- name: v.name,
149
- scope: v.scope,
150
- label: v.label ?? null,
151
- oauth: isOAuth,
152
- adminConfigured: st?.adminConfigured ?? false,
153
- userConfigured: st?.userConfigured ?? false,
154
- authorized: isOAuth ? (st?.userAuthorized ?? false) : null,
155
- };
156
- }),
157
- })),
158
- );
159
- return { tools };
160
- }),
161
- );
162
- }
163
-
164
- /** "Local-only tools configured for you: `a`, `b`." (or a none note), filtered to what the caller may read. */
165
- async function localToolsLine(svc: IToolManualService, userEmail?: string): Promise<string> {
166
- if (!userEmail) return 'No local-only tools are currently configured.';
167
- const tools = await svc.listLocalOnly(userEmail);
168
- if (tools.length === 0) return 'No local-only tools are currently configured.';
169
- return `Local-only tools configured for you: ${tools.map((t) => `\`${t.name}\``).join(', ')}.`;
170
- }
171
-
172
- async function buildListLocalToolsDef(svc: IToolManualService, userEmail?: string): Promise<UtcpTool> {
173
- return toolDef({
174
- name: 'list_local_tools',
175
- description:
176
- 'List tools your workspace admins configured that run ONLY in a local environment ' +
177
- '(e.g. a self-hosted MCP server on localhost) and therefore cannot be called through this ' +
178
- 'remote endpoint. Each entry gives the tool’s name and its `.tool` file path in the knowledge ' +
179
- 'base — read that file with `read_file` and configure the tool yourself in your local setup ' +
180
- '(e.g. add the MCP server to your client). ' +
181
- (await localToolsLine(svc, userEmail)),
182
- path: '/api/agent/tools/list_local_tools',
183
- inputs: { type: 'object', properties: {}, additionalProperties: false },
184
- outputs: {
185
- type: 'object',
186
- properties: {
187
- tools: {
188
- type: 'array',
189
- description: 'Local-only tools (name + KB path).',
190
- items: {
191
- type: 'object',
192
- properties: {
193
- name: { type: 'string' },
194
- path: { type: 'string', description: 'KB path of the `.tool` file (read it with read_file).' },
195
- },
196
- },
197
- },
198
- },
199
- },
200
- tags: ['tools'],
201
- });
202
- }
1
+ import type { Router, RequestHandler } from 'express';
2
+ import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
3
+ import type { IToolRegistry, UtcpTool } from '../tool-registry/tool.contract.js';
4
+ import type { ToolContext } from '../tool-helpers/tool.contract.js';
5
+ import { toolDef } from '../tool-helpers/tool-def.js';
6
+ import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
7
+ import type { IAccessControl } from '../access/access-control.interface.js';
8
+ import { workspaceIdForBranch } from '../workspace/workspace.service.js';
9
+ import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
10
+ import type { IToolManualService } from './tool-manuals.contract.js';
11
+
12
+ /**
13
+ * What `list_tool_setup` needs from the secrets vault (structurally satisfied
14
+ * by `ISecretsVaultService.statusFor`). A local port keeps this module free of
15
+ * a secrets-vault import — the same decoupling discipline as
16
+ * `McpAuthDiscoveryPort` in the service.
17
+ */
18
+ export interface VariableStatusPort {
19
+ statusFor(
20
+ userId: string,
21
+ keys: string[],
22
+ ): Promise<{ key: string; adminConfigured: boolean; userConfigured: boolean; userAuthorized?: boolean }[]>;
23
+ }
24
+
25
+ /**
26
+ * Registers the tool-manual catalog tools (both surfaces) and hosts their
27
+ * endpoints:
28
+ *
29
+ * - `list_local_tools` — local-only manuals (`remote: false`) the remote proxy
30
+ * can't serve; the agent reads their `.tool` path and self-configures.
31
+ * - `list_tool_setup` — configuration status of every accessible manual, so an
32
+ * agent can EXPLAIN the remaining setup to the admin. Deliberately
33
+ * read-only: secret VALUES are never set (or returned) through a tool — the
34
+ * admin pastes them into the tool editor; users sign in on /connect.
35
+ */
36
+ export function registerToolManualsTools(
37
+ registry: IToolRegistry,
38
+ router: Router,
39
+ toolAuth: RequestHandler,
40
+ toolHandler: ToolHandlerFactory,
41
+ toolManualService: IToolManualService,
42
+ deps: {
43
+ accessControl: IAccessControl;
44
+ variableStatus: VariableStatusPort;
45
+ },
46
+ ): void {
47
+ registry.registerExternalTool((ctx) => buildListLocalToolsDef(toolManualService, ctx.userEmail));
48
+ registry.registerInternalTool((ctx) => buildListLocalToolsDef(toolManualService, ctx.userEmail));
49
+
50
+ router.post(
51
+ '/agent/tools/list_local_tools',
52
+ toolAuth,
53
+ toolHandler(async (_args, ctx: ToolContext) => ({
54
+ tools: await toolManualService.listLocalOnly(ctx.user.email),
55
+ })),
56
+ );
57
+
58
+ // A FUNCTION, not a constant — this factory runs at boot, and on a
59
+ // setup-screen deployment the branch model is applied AFTER boot. Only a
60
+ // read inside a handler body sees the configured `DEFAULT_BRANCH`; a
61
+ // construction-time capture would hold the empty pre-setup id until restart.
62
+ const defaultWs = () => workspaceIdForBranch(DEFAULT_BRANCH);
63
+ const varKey = (manualName: string, varName: string) => utcpNamespacedKey(manualName, varName);
64
+
65
+ const listSetupDef = toolDef({
66
+ name: 'list_tool_setup',
67
+ description:
68
+ 'Configuration status of every `.tool` the current user can access: what each tool needs set up and ' +
69
+ 'what is already configured. Results are scoped to the caller — a `.tool` the caller cannot READ is ' +
70
+ 'absent entirely, and all status flags reflect the caller\'s own state. Per tool: `setup` describes ' +
71
+ 'an MCP server\'s sign-in requirement (`open` = none; `oauth-auto` = sign-in was configured ' +
72
+ 'automatically; `oauth-manual` = the provider does not support automatic registration, so a writer ' +
73
+ 'must declare the OAuth provider in the `.tool` file and paste its client secret into the tool ' +
74
+ 'editor). Per variable: whether the shared (admin) value is set, whether the CURRENT user has ' +
75
+ 'set/authorized their own, and whether it is an OAuth sign-in (users authorize those on the /connect ' +
76
+ 'page, never by typing a value). `canWrite` = the caller may write THAT `.tool` FILE (per-file access ' +
77
+ 'from its frontmatter `write:`/`owner:` verbs and the access.md chain — NOT a platform role), which ' +
78
+ 'is exactly what gates setting its shared secrets: the people who manage the file configure the tool. ' +
79
+ 'Secret VALUES are never returned and can never be set through a tool — an admin enters them in the ' +
80
+ 'tool editor; users sign in on /connect.',
81
+ path: '/api/agent/tools/list_tool_setup',
82
+ inputs: { type: 'object', properties: {}, required: [], additionalProperties: false },
83
+ outputs: {
84
+ type: 'object',
85
+ properties: {
86
+ tools: {
87
+ type: 'array',
88
+ items: {
89
+ type: 'object',
90
+ properties: {
91
+ slug: { type: 'string' },
92
+ name: { type: 'string', description: 'The manual id — the namespace secrets bind to.' },
93
+ path: { type: 'string' },
94
+ type: { type: 'string' },
95
+ setup: {
96
+ type: ['object', 'null'],
97
+ description: 'MCP auto-discovery setup requirement; null for non-mcp tools.',
98
+ properties: { kind: { type: 'string' }, reason: { type: 'string' } },
99
+ },
100
+ canWrite: { type: 'boolean' },
101
+ variables: {
102
+ type: 'array',
103
+ items: {
104
+ type: 'object',
105
+ properties: {
106
+ name: { type: 'string' },
107
+ scope: { type: 'string', description: '`admin` (one shared value) or `user` (per user).' },
108
+ label: { type: ['string', 'null'] },
109
+ oauth: { type: 'boolean', description: 'Filled by signing in on /connect, not by a value.' },
110
+ adminConfigured: {
111
+ type: 'boolean',
112
+ description:
113
+ 'Shared value set (plain admin vars), or the owner finished provider setup (OAuth vars).',
114
+ },
115
+ userConfigured: { type: 'boolean' },
116
+ authorized: { type: ['boolean', 'null'], description: 'OAuth vars: current user signed in.' },
117
+ },
118
+ required: ['name', 'scope', 'oauth', 'adminConfigured', 'userConfigured'],
119
+ },
120
+ },
121
+ },
122
+ required: ['slug', 'name', 'path', 'type', 'canWrite', 'variables'],
123
+ },
124
+ },
125
+ },
126
+ required: ['tools'],
127
+ },
128
+ tags: ['tools'],
129
+ });
130
+ registry.registerInternalTool(listSetupDef);
131
+ registry.registerExternalTool(listSetupDef);
132
+ router.post(
133
+ '/agent/tools/list_tool_setup',
134
+ toolAuth,
135
+ toolHandler(async (_args, ctx: ToolContext) => {
136
+ const manuals = await toolManualService.listAccessible(ctx.user.email);
137
+ const allKeys = manuals.flatMap((m) => (m.variables ?? []).map((v) => varKey(m.name, v.name)));
138
+ const status = await deps.variableStatus.statusFor(ctx.user.id, allKeys);
139
+ const statusByKey = new Map(status.map((s) => [s.key, s]));
140
+ const tools = await Promise.all(
141
+ manuals.map(async (m) => ({
142
+ slug: m.slug,
143
+ name: m.name,
144
+ path: m.path,
145
+ type: m.type,
146
+ setup: m.setup ?? null,
147
+ canWrite: await deps.accessControl.canWrite(defaultWs(), ctx.user.email, m.path),
148
+ variables: (m.variables ?? []).map((v) => {
149
+ const st = statusByKey.get(varKey(m.name, v.name));
150
+ const isOAuth = v.oauth != null;
151
+ return {
152
+ name: v.name,
153
+ scope: v.scope,
154
+ label: v.label ?? null,
155
+ oauth: isOAuth,
156
+ adminConfigured: st?.adminConfigured ?? false,
157
+ userConfigured: st?.userConfigured ?? false,
158
+ authorized: isOAuth ? (st?.userAuthorized ?? false) : null,
159
+ };
160
+ }),
161
+ })),
162
+ );
163
+ return { tools };
164
+ }),
165
+ );
166
+ }
167
+
168
+ /** "Local-only tools configured for you: `a`, `b`." (or a none note), filtered to what the caller may read. */
169
+ async function localToolsLine(svc: IToolManualService, userEmail?: string): Promise<string> {
170
+ if (!userEmail) return 'No local-only tools are currently configured.';
171
+ const tools = await svc.listLocalOnly(userEmail);
172
+ if (tools.length === 0) return 'No local-only tools are currently configured.';
173
+ return `Local-only tools configured for you: ${tools.map((t) => `\`${t.name}\``).join(', ')}.`;
174
+ }
175
+
176
+ async function buildListLocalToolsDef(svc: IToolManualService, userEmail?: string): Promise<UtcpTool> {
177
+ return toolDef({
178
+ name: 'list_local_tools',
179
+ description:
180
+ 'List tools your workspace admins configured that run ONLY in a local environment ' +
181
+ '(e.g. a self-hosted MCP server on localhost) and therefore cannot be called through this ' +
182
+ 'remote endpoint. Each entry gives the tool’s name and its `.tool` file path in the knowledge ' +
183
+ 'base — read that file with `read_file` and configure the tool yourself in your local setup ' +
184
+ '(e.g. add the MCP server to your client). ' +
185
+ (await localToolsLine(svc, userEmail)),
186
+ path: '/api/agent/tools/list_local_tools',
187
+ inputs: { type: 'object', properties: {}, additionalProperties: false },
188
+ outputs: {
189
+ type: 'object',
190
+ properties: {
191
+ tools: {
192
+ type: 'array',
193
+ description: 'Local-only tools (name + KB path).',
194
+ items: {
195
+ type: 'object',
196
+ properties: {
197
+ name: { type: 'string' },
198
+ path: { type: 'string', description: 'KB path of the `.tool` file (read it with read_file).' },
199
+ },
200
+ },
201
+ },
202
+ },
203
+ },
204
+ tags: ['tools'],
205
+ });
206
+ }