ruflo 3.42.3 → 3.42.4

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 (479) hide show
  1. package/README.md +422 -422
  2. package/bin/ruflo.js +77 -77
  3. package/package.json +117 -117
  4. package/src/chat-ui/Dockerfile +25 -25
  5. package/src/chat-ui/patch-mcp-url-safety.sh +28 -28
  6. package/src/config/config.example.json +76 -76
  7. package/src/mcp-bridge/Dockerfile +47 -47
  8. package/src/mcp-bridge/index.js +1780 -1780
  9. package/src/mcp-bridge/mcp-stdio-kernel.js +159 -159
  10. package/src/mcp-bridge/package.json +17 -17
  11. package/src/mcp-bridge/test-harness.js +470 -470
  12. package/src/mcp-bridge/test-runtime-security.mjs +208 -208
  13. package/src/mcp-bridge/test-security-lock.js +121 -121
  14. package/src/nginx/Dockerfile +10 -10
  15. package/src/nginx/nginx.conf +67 -67
  16. package/src/nginx/static/favicon-dark.svg +4 -4
  17. package/src/nginx/static/favicon.svg +4 -4
  18. package/src/nginx/static/icon.svg +5 -5
  19. package/src/nginx/static/logo.svg +9 -9
  20. package/src/nginx/static/manifest.json +22 -22
  21. package/src/nginx/static/welcome.js +184 -184
  22. package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
  23. package/src/ruvocal/.devcontainer/Dockerfile +9 -9
  24. package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
  25. package/src/ruvocal/.dockerignore +16 -16
  26. package/src/ruvocal/.eslintignore +13 -13
  27. package/src/ruvocal/.eslintrc.cjs +45 -45
  28. package/src/ruvocal/.gcloudignore +18 -18
  29. package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
  30. package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
  31. package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
  32. package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
  33. package/src/ruvocal/.github/release.yml +16 -16
  34. package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
  35. package/src/ruvocal/.github/workflows/build-image.yml +142 -142
  36. package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
  37. package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
  38. package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
  39. package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
  40. package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
  41. package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
  42. package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
  43. package/src/ruvocal/.husky/lint-stage-config.js +4 -4
  44. package/src/ruvocal/.husky/pre-commit +2 -2
  45. package/src/ruvocal/.prettierignore +14 -14
  46. package/src/ruvocal/.prettierrc +7 -7
  47. package/src/ruvocal/CLAUDE.md +126 -126
  48. package/src/ruvocal/Dockerfile +96 -96
  49. package/src/ruvocal/LICENSE +202 -202
  50. package/src/ruvocal/PRIVACY.md +41 -41
  51. package/src/ruvocal/README.md +164 -164
  52. package/src/ruvocal/chart/Chart.yaml +5 -5
  53. package/src/ruvocal/chart/env/dev.yaml +260 -260
  54. package/src/ruvocal/chart/env/prod.yaml +273 -273
  55. package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
  56. package/src/ruvocal/chart/templates/config.yaml +10 -10
  57. package/src/ruvocal/chart/templates/deployment.yaml +81 -81
  58. package/src/ruvocal/chart/templates/hpa.yaml +45 -45
  59. package/src/ruvocal/chart/templates/infisical.yaml +24 -24
  60. package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
  61. package/src/ruvocal/chart/templates/ingress.yaml +32 -32
  62. package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
  63. package/src/ruvocal/chart/templates/service-account.yaml +13 -13
  64. package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
  65. package/src/ruvocal/chart/templates/service.yaml +21 -21
  66. package/src/ruvocal/chart/values.yaml +73 -73
  67. package/src/ruvocal/cloudbuild.yaml +68 -68
  68. package/src/ruvocal/config/branding.env.example +19 -19
  69. package/src/ruvocal/docker-compose.yml +21 -21
  70. package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
  71. package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
  72. package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
  73. package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
  74. package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
  75. package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
  76. package/src/ruvocal/docs/source/_toctree.yml +30 -30
  77. package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
  78. package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
  79. package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
  80. package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
  81. package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
  82. package/src/ruvocal/docs/source/configuration/overview.md +89 -89
  83. package/src/ruvocal/docs/source/configuration/theming.md +20 -20
  84. package/src/ruvocal/docs/source/developing/architecture.md +48 -48
  85. package/src/ruvocal/docs/source/index.md +53 -53
  86. package/src/ruvocal/docs/source/installation/docker.md +43 -43
  87. package/src/ruvocal/docs/source/installation/helm.md +43 -43
  88. package/src/ruvocal/docs/source/installation/local.md +62 -62
  89. package/src/ruvocal/entrypoint.sh +18 -18
  90. package/src/ruvocal/mcp-bridge/Dockerfile +47 -47
  91. package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
  92. package/src/ruvocal/mcp-bridge/index.js +1995 -1995
  93. package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
  94. package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
  95. package/src/ruvocal/mcp-bridge/package.json +17 -17
  96. package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
  97. package/src/ruvocal/package-lock.json +11741 -11741
  98. package/src/ruvocal/package.json +121 -121
  99. package/src/ruvocal/postcss.config.js +6 -6
  100. package/src/ruvocal/rvf.manifest.json +204 -204
  101. package/src/ruvocal/scripts/config.ts +64 -64
  102. package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
  103. package/src/ruvocal/scripts/populate.ts +288 -288
  104. package/src/ruvocal/scripts/samples.txt +194 -194
  105. package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
  106. package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
  107. package/src/ruvocal/src/ambient.d.ts +7 -7
  108. package/src/ruvocal/src/app.d.ts +29 -29
  109. package/src/ruvocal/src/app.html +53 -53
  110. package/src/ruvocal/src/hooks.server.ts +32 -32
  111. package/src/ruvocal/src/hooks.ts +6 -6
  112. package/src/ruvocal/src/lib/APIClient.ts +148 -148
  113. package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
  114. package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
  115. package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
  116. package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
  117. package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
  118. package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
  119. package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
  120. package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
  121. package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
  122. package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
  123. package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
  124. package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
  125. package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
  126. package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
  127. package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
  128. package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
  129. package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
  130. package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
  131. package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
  132. package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
  133. package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
  134. package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
  135. package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
  136. package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
  137. package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
  138. package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
  139. package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
  140. package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
  141. package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
  142. package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
  143. package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
  144. package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
  145. package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
  146. package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
  147. package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
  148. package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
  149. package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
  150. package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
  151. package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
  152. package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
  153. package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
  154. package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
  155. package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
  156. package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
  157. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
  158. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
  159. package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
  160. package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
  161. package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
  162. package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
  163. package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
  164. package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
  165. package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
  166. package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
  167. package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
  168. package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
  169. package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
  170. package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
  171. package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
  172. package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
  173. package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
  174. package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
  175. package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
  176. package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
  177. package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
  178. package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
  179. package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
  180. package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
  181. package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
  182. package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
  183. package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
  184. package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
  185. package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
  186. package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
  187. package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
  188. package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
  189. package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
  190. package/src/ruvocal/src/lib/constants/mime.ts +11 -11
  191. package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
  192. package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
  193. package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
  194. package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
  195. package/src/ruvocal/src/lib/createShareLink.ts +27 -27
  196. package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
  197. package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
  198. package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
  199. package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
  200. package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
  201. package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
  202. package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
  203. package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
  204. package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
  205. package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
  206. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
  207. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
  208. package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
  209. package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
  210. package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
  211. package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
  212. package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
  213. package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
  214. package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
  215. package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
  216. package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
  217. package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
  218. package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
  219. package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
  220. package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
  221. package/src/ruvocal/src/lib/server/api/types.ts +37 -37
  222. package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
  223. package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
  224. package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
  225. package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
  226. package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
  227. package/src/ruvocal/src/lib/server/auth.ts +554 -554
  228. package/src/ruvocal/src/lib/server/config.ts +187 -187
  229. package/src/ruvocal/src/lib/server/conversation.ts +83 -83
  230. package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
  231. package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
  232. package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
  233. package/src/ruvocal/src/lib/server/database.ts +145 -145
  234. package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
  235. package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
  236. package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
  237. package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
  238. package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
  239. package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
  240. package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
  241. package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
  242. package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
  243. package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
  244. package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
  245. package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
  246. package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
  247. package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
  248. package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
  249. package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
  250. package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
  251. package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
  252. package/src/ruvocal/src/lib/server/logger.ts +42 -42
  253. package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
  254. package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
  255. package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
  256. package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
  257. package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
  258. package/src/ruvocal/src/lib/server/metrics.ts +255 -255
  259. package/src/ruvocal/src/lib/server/models.ts +518 -518
  260. package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
  261. package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
  262. package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
  263. package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
  264. package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
  265. package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
  266. package/src/ruvocal/src/lib/server/router/types.ts +21 -21
  267. package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
  268. package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
  269. package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
  270. package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
  271. package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
  272. package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
  273. package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
  274. package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
  275. package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
  276. package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
  277. package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
  278. package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
  279. package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
  280. package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
  281. package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
  282. package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
  283. package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
  284. package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
  285. package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
  286. package/src/ruvocal/src/lib/stores/errors.ts +9 -9
  287. package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
  288. package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
  289. package/src/ruvocal/src/lib/stores/loading.ts +3 -3
  290. package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
  291. package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
  292. package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
  293. package/src/ruvocal/src/lib/stores/settings.ts +182 -182
  294. package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
  295. package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
  296. package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
  297. package/src/ruvocal/src/lib/switchTheme.ts +124 -124
  298. package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
  299. package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
  300. package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
  301. package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
  302. package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
  303. package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
  304. package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
  305. package/src/ruvocal/src/lib/types/Message.ts +41 -41
  306. package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
  307. package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
  308. package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
  309. package/src/ruvocal/src/lib/types/Model.ts +23 -23
  310. package/src/ruvocal/src/lib/types/Report.ts +12 -12
  311. package/src/ruvocal/src/lib/types/Review.ts +6 -6
  312. package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
  313. package/src/ruvocal/src/lib/types/Session.ts +22 -22
  314. package/src/ruvocal/src/lib/types/Settings.ts +93 -93
  315. package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
  316. package/src/ruvocal/src/lib/types/Template.ts +6 -6
  317. package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
  318. package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
  319. package/src/ruvocal/src/lib/types/Tool.ts +77 -77
  320. package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
  321. package/src/ruvocal/src/lib/types/User.ts +14 -14
  322. package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
  323. package/src/ruvocal/src/lib/utils/auth.ts +17 -17
  324. package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
  325. package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
  326. package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
  327. package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
  328. package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
  329. package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
  330. package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
  331. package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
  332. package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
  333. package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
  334. package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
  335. package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
  336. package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
  337. package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
  338. package/src/ruvocal/src/lib/utils/hf.ts +17 -17
  339. package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
  340. package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
  341. package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
  342. package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
  343. package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
  344. package/src/ruvocal/src/lib/utils/marked.ts +531 -531
  345. package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
  346. package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
  347. package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +286 -286
  348. package/src/ruvocal/src/lib/utils/messageUpdates.ts +332 -332
  349. package/src/ruvocal/src/lib/utils/mime.ts +56 -56
  350. package/src/ruvocal/src/lib/utils/models.ts +14 -14
  351. package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
  352. package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
  353. package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
  354. package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
  355. package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
  356. package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
  357. package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
  358. package/src/ruvocal/src/lib/utils/sum.ts +3 -3
  359. package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
  360. package/src/ruvocal/src/lib/utils/template.ts +53 -53
  361. package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
  362. package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
  363. package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
  364. package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
  365. package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
  366. package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
  367. package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
  368. package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
  369. package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
  370. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
  371. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
  372. package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
  373. package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
  374. package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
  375. package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
  376. package/src/ruvocal/src/lib/utils/updates.ts +39 -39
  377. package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
  378. package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
  379. package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
  380. package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
  381. package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
  382. package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
  383. package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
  384. package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
  385. package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
  386. package/src/ruvocal/src/routes/+error.svelte +20 -20
  387. package/src/ruvocal/src/routes/+layout.svelte +324 -324
  388. package/src/ruvocal/src/routes/+layout.ts +91 -91
  389. package/src/ruvocal/src/routes/+page.svelte +168 -168
  390. package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
  391. package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
  392. package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
  393. package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
  394. package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
  395. package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
  396. package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
  397. package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
  398. package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
  399. package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
  400. package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
  401. package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
  402. package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
  403. package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
  404. package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
  405. package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
  406. package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
  407. package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
  408. package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
  409. package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
  410. package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
  411. package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
  412. package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
  413. package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
  414. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
  415. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
  416. package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
  417. package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
  418. package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
  419. package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
  420. package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
  421. package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
  422. package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
  423. package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
  424. package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
  425. package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +593 -593
  426. package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
  427. package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
  428. package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
  429. package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
  430. package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
  431. package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
  432. package/src/ruvocal/src/routes/login/+server.ts +5 -5
  433. package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
  434. package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
  435. package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
  436. package/src/ruvocal/src/routes/logout/+server.ts +18 -18
  437. package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
  438. package/src/ruvocal/src/routes/models/+page.svelte +233 -233
  439. package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
  440. package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
  441. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
  442. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
  443. package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
  444. package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
  445. package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
  446. package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
  447. package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
  448. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
  449. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
  450. package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
  451. package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
  452. package/src/ruvocal/src/styles/highlight-js.css +195 -195
  453. package/src/ruvocal/src/styles/main.css +144 -144
  454. package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
  455. package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
  456. package/src/ruvocal/static/chatui/favicon.svg +3 -3
  457. package/src/ruvocal/static/chatui/icon.svg +3 -3
  458. package/src/ruvocal/static/chatui/logo.svg +7 -7
  459. package/src/ruvocal/static/chatui/manifest.json +54 -54
  460. package/src/ruvocal/static/chatui/welcome.js +184 -184
  461. package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
  462. package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
  463. package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
  464. package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
  465. package/src/ruvocal/static/huggingchat/icon.svg +4 -4
  466. package/src/ruvocal/static/huggingchat/logo.svg +4 -4
  467. package/src/ruvocal/static/huggingchat/manifest.json +54 -54
  468. package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
  469. package/src/ruvocal/static/robots.txt +10 -10
  470. package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
  471. package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
  472. package/src/ruvocal/svelte.config.js +53 -53
  473. package/src/ruvocal/tailwind.config.cjs +30 -30
  474. package/src/ruvocal/tsconfig.json +19 -19
  475. package/src/ruvocal/vite.config.ts +87 -87
  476. package/src/scripts/deploy.sh +116 -116
  477. package/src/scripts/generate-config.js +245 -245
  478. package/src/scripts/generate-welcome.js +187 -187
  479. package/src/scripts/package-rvf.sh +116 -116
@@ -1,1780 +1,1780 @@
1
- import express from "express";
2
- import { spawn } from "child_process";
3
- import { randomUUID, timingSafeEqual } from "crypto";
4
-
5
- // =============================================================================
6
- // CONFIGURATION
7
- // =============================================================================
8
-
9
- const CLOUD_FUNCTIONS = {
10
- // search: process.env.SEARCH_API_URL || "https://my-search-api.run.app",
11
- // research: process.env.RESEARCH_API_URL || "https://my-research-api.run.app",
12
- };
13
-
14
- const PORT = parseInt(process.env.PORT || "3001", 10);
15
- const BIND_HOST = process.env.MCP_BIND_HOST || "127.0.0.1";
16
-
17
- // =============================================================================
18
- // TOOL GROUPS — Enable/disable categories of tools independently
19
- // =============================================================================
20
- // Groups map tool name prefixes from backends to logical categories.
21
- // Each group can be toggled via env var. The AI sees only enabled tools.
22
-
23
- const TOOL_GROUPS = {
24
- // --- Core (always on, built-in) ---
25
- core: {
26
- enabled: true, // cannot be disabled
27
- description: "Search, research, and guidance tools",
28
- source: "builtin",
29
- },
30
-
31
- // --- Intelligence (ruvector) ---
32
- intelligence: {
33
- enabled: process.env.MCP_GROUP_INTELLIGENCE !== "false",
34
- description: "Self-learning intelligence — routing, memory, pattern training (ruvector)",
35
- source: "ruvector",
36
- prefixes: ["hooks_"],
37
- },
38
-
39
- // --- Agents & Orchestration (ruflo) ---
40
- agents: {
41
- enabled: process.env.MCP_GROUP_AGENTS !== "false",
42
- description: "Agent lifecycle, swarm coordination, task management, workflows (ruflo)",
43
- source: "ruflo",
44
- prefixes: ["agent_", "swarm_", "task_", "session_", "hive-mind_", "workflow_", "coordination_"],
45
- },
46
-
47
- // --- Memory & Knowledge (ruflo) ---
48
- memory: {
49
- enabled: process.env.MCP_GROUP_MEMORY !== "false",
50
- description: "Vector memory, AgentDB, embeddings, semantic search (ruflo)",
51
- source: "ruflo",
52
- prefixes: ["memory_", "agentdb_", "embeddings_"],
53
- },
54
-
55
- // --- Dev Tools (ruflo) ---
56
- devtools: {
57
- enabled: process.env.MCP_GROUP_DEVTOOLS !== "false",
58
- description: "Hooks, code analysis, performance profiling, GitHub integration (ruflo)",
59
- source: "ruflo",
60
- prefixes: ["hooks_", "analyze_", "performance_", "github_", "terminal_", "config_", "system_", "progress_"],
61
- },
62
-
63
- // --- Security & Safety (ruflo) ---
64
- security: {
65
- enabled: process.env.MCP_GROUP_SECURITY === "true",
66
- description: "AI defence, PII detection, claims management, pattern transfer (ruflo)",
67
- source: "ruflo",
68
- prefixes: ["aidefence_", "claims_", "transfer_"],
69
- },
70
-
71
- // --- Browser Automation (ruflo) ---
72
- browser: {
73
- enabled: process.env.MCP_GROUP_BROWSER === "true",
74
- description: "Headless browser control — navigate, click, fill, screenshot (ruflo)",
75
- source: "ruflo",
76
- prefixes: ["browser_"],
77
- },
78
-
79
- // --- Neural & DAA (ruflo) ---
80
- neural: {
81
- enabled: process.env.MCP_GROUP_NEURAL === "true",
82
- description: "Neural network training, DAA autonomous agents, cognitive patterns (ruflo)",
83
- source: "ruflo",
84
- prefixes: ["neural_", "daa_"],
85
- },
86
-
87
- // --- Agentic Flow (agentic-flow@alpha) ---
88
- "agentic-flow": {
89
- enabled: process.env.MCP_GROUP_AGENTIC_FLOW === "true",
90
- description: "Execute 66+ specialized agents, batch code editing, AgentDB patterns (agentic-flow)",
91
- source: "agentic-flow",
92
- prefixes: ["agentic_flow_", "agent_booster_", "agentdb_"],
93
- },
94
-
95
- // --- Claude Code ---
96
- "claude-code": {
97
- enabled: process.env.MCP_GROUP_CLAUDE_CODE === "true",
98
- description: "Anthropic Claude Code — file editing, bash execution, code analysis (requires ANTHROPIC_API_KEY)",
99
- source: "claude",
100
- },
101
-
102
- // --- Gemini MCP ---
103
- gemini: {
104
- enabled: process.env.MCP_GROUP_GEMINI === "true",
105
- description: "Google Gemini conversation context, multimodal capabilities (requires GOOGLE_API_KEY)",
106
- source: "gemini-mcp",
107
- },
108
-
109
- // --- OpenAI Codex ---
110
- codex: {
111
- enabled: process.env.MCP_GROUP_CODEX === "true",
112
- description: "OpenAI Codex coding agent — code generation and execution (requires OPENAI_API_KEY)",
113
- source: "codex",
114
- },
115
- };
116
-
117
- // =============================================================================
118
- // STDIO MCP CLIENT — Connects to external MCP servers via child process
119
- // =============================================================================
120
-
121
- class StdioMcpClient {
122
- constructor(name, command, args = []) {
123
- this.name = name;
124
- this.command = command;
125
- this.args = args;
126
- this.process = null;
127
- this.tools = [];
128
- this.ready = false;
129
- this.pending = new Map();
130
- this.buffer = "";
131
- }
132
-
133
- async start() {
134
- return new Promise((resolve) => {
135
- try {
136
- this.process = spawn(this.command, this.args, {
137
- stdio: ["pipe", "pipe", "pipe"],
138
- env: { ...process.env },
139
- });
140
-
141
- this.process.stdout.on("data", (data) => this._onData(data.toString()));
142
- this.process.stderr.on("data", (data) => {
143
- const msg = data.toString().trim();
144
- if (msg && !msg.startsWith("npm WARN")) console.error(`[${this.name}] ${msg}`);
145
- });
146
- this.process.on("error", (err) => {
147
- console.error(`[${this.name}] spawn error:`, err.message);
148
- this.ready = false;
149
- resolve(false);
150
- });
151
- this.process.on("exit", (code) => {
152
- console.log(`[${this.name}] exited with code ${code}`);
153
- this.ready = false;
154
- });
155
-
156
- this._send("initialize", {
157
- protocolVersion: "2024-11-05",
158
- capabilities: {},
159
- clientInfo: { name: "mcp-bridge", version: "2.0.0" },
160
- }).then((result) => {
161
- if (result && !result.error) {
162
- this._notify("notifications/initialized", {});
163
- return this._send("tools/list", {});
164
- }
165
- return null;
166
- }).then((result) => {
167
- if (result && result.tools) {
168
- this.tools = result.tools.map(t => ({
169
- ...t,
170
- _originalName: t.name,
171
- _backend: this.name,
172
- }));
173
- this.ready = true;
174
- console.log(`[${this.name}] ${this.tools.length} tools loaded`);
175
- }
176
- resolve(this.ready);
177
- }).catch((err) => {
178
- console.error(`[${this.name}] init failed:`, err.message);
179
- resolve(false);
180
- });
181
-
182
- setTimeout(() => { if (!this.ready) resolve(false); }, 60000);
183
- } catch (err) {
184
- console.error(`[${this.name}] failed to start:`, err.message);
185
- resolve(false);
186
- }
187
- });
188
- }
189
-
190
- _onData(chunk) {
191
- this.buffer += chunk;
192
- const lines = this.buffer.split("\n");
193
- this.buffer = lines.pop() || "";
194
- for (const line of lines) {
195
- const trimmed = line.trim();
196
- if (!trimmed) continue;
197
- try {
198
- const msg = JSON.parse(trimmed);
199
- if (msg.id && this.pending.has(msg.id)) {
200
- const { resolve } = this.pending.get(msg.id);
201
- this.pending.delete(msg.id);
202
- resolve(msg.result || msg.error || {});
203
- }
204
- } catch { /* skip non-JSON */ }
205
- }
206
- }
207
-
208
- _send(method, params) {
209
- return new Promise((resolve, reject) => {
210
- if (!this.process || this.process.killed) {
211
- return reject(new Error(`${this.name} process not running`));
212
- }
213
- const id = randomUUID();
214
- const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
215
- this.pending.set(id, { resolve, reject });
216
- this.process.stdin.write(msg);
217
- setTimeout(() => {
218
- if (this.pending.has(id)) {
219
- this.pending.delete(id);
220
- reject(new Error(`${this.name} timeout for ${method}`));
221
- }
222
- }, 30000);
223
- });
224
- }
225
-
226
- _notify(method, params) {
227
- if (!this.process || this.process.killed) return;
228
- this.process.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
229
- }
230
-
231
- async callTool(originalName, args) {
232
- if (!this.ready) return { error: `${this.name} backend not available` };
233
- try {
234
- return await this._send("tools/call", { name: originalName, arguments: args });
235
- } catch (err) {
236
- return { error: err.message };
237
- }
238
- }
239
-
240
- stop() {
241
- if (this.process && !this.process.killed) {
242
- this.process.kill("SIGTERM");
243
- this.process = null;
244
- }
245
- this.ready = false;
246
- this.tools = [];
247
- }
248
- }
249
-
250
- // =============================================================================
251
- // BACKEND REGISTRY
252
- // =============================================================================
253
-
254
- const BACKEND_DEFS = [
255
- { name: "ruvector", command: "npx", args: ["-y", "ruvector", "mcp", "start"], groups: ["intelligence"] },
256
- { name: "ruflo", command: "npx", args: ["-y", "ruflo", "mcp", "start"], groups: ["agents", "memory", "devtools", "security", "browser", "neural"] },
257
- { name: "agentic-flow", command: "npx", args: ["-y", "agentic-flow@alpha", "mcp", "start"], groups: ["agentic-flow"] },
258
- { name: "claude", command: "claude", args: ["mcp", "serve"], groups: ["claude-code"] },
259
- { name: "gemini-mcp", command: "npx", args: ["-y", "gemini-mcp-server"], groups: ["gemini"] },
260
- { name: "codex", command: "npx", args: ["-y", "@openai/codex", "mcp-server"], groups: ["codex"] },
261
- ];
262
-
263
- const mcpBackends = new Map();
264
- let allBackendTools = []; // all tools from all backends (pre-filter)
265
-
266
- function isBackendNeeded(backendDef) {
267
- return backendDef.groups.some(g => TOOL_GROUPS[g]?.enabled);
268
- }
269
-
270
- // Filter tools from a backend based on which groups are enabled
271
- function filterToolsByGroups(tools, backendName) {
272
- const enabledGroups = Object.entries(TOOL_GROUPS)
273
- .filter(([, g]) => g.enabled && g.source === backendName);
274
-
275
- if (enabledGroups.length === 0) return [];
276
-
277
- // If any enabled group has no prefixes defined, include all tools from that backend
278
- const hasWildcard = enabledGroups.some(([, g]) => !g.prefixes);
279
- if (hasWildcard) return tools;
280
-
281
- const enabledPrefixes = enabledGroups.flatMap(([, g]) => g.prefixes || []);
282
- return tools.filter(t => enabledPrefixes.some(p => t._originalName.startsWith(p)));
283
- }
284
-
285
- // Get the final filtered tool list with namespaced names
286
- function getActiveTools() {
287
- const filtered = [];
288
- for (const [backendName, client] of mcpBackends) {
289
- const accepted = filterToolsByGroups(client.tools, backendName);
290
- for (const t of accepted) {
291
- filtered.push({ ...t, name: `${backendName}__${t._originalName}` });
292
- }
293
- }
294
- return filtered;
295
- }
296
-
297
- async function initBackends() {
298
- const needed = BACKEND_DEFS.filter(isBackendNeeded);
299
- if (needed.length === 0) return;
300
-
301
- console.log(`Starting ${needed.length} MCP backends: ${needed.map(b => b.name).join(", ")}`);
302
-
303
- await Promise.allSettled(
304
- needed.map(async (b) => {
305
- const client = new StdioMcpClient(b.name, b.command, b.args);
306
- const ok = await client.start();
307
- if (ok) {
308
- mcpBackends.set(b.name, client);
309
- } else {
310
- console.warn(`[${b.name}] failed to start`);
311
- }
312
- })
313
- );
314
-
315
- allBackendTools = getActiveTools();
316
- console.log(`MCP backends: ${mcpBackends.size} active, ${allBackendTools.length} tools (filtered by groups)`);
317
- }
318
-
319
- process.on("SIGTERM", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
320
- process.on("SIGINT", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
321
-
322
- // =============================================================================
323
- // BUILT-IN TOOLS (core group — always on)
324
- // =============================================================================
325
-
326
- const BUILTIN_TOOLS = [
327
- {
328
- name: "search",
329
- description: "Search your knowledge base for relevant information.",
330
- inputSchema: {
331
- type: "object",
332
- properties: {
333
- query: { type: "string", description: "Natural language search query" },
334
- limit: { type: "number", description: "Max results (default 5)", default: 5 },
335
- },
336
- required: ["query"],
337
- },
338
- },
339
- {
340
- name: "web_research",
341
- description: "Search the web, fact-check claims, compare items, or conduct deep research. Actions: 'search' (quick), 'research' (deep report), 'compare' (side-by-side), 'fact_check' (verify claims), 'goap' (comprehensive multi-step research with verification).",
342
- inputSchema: {
343
- type: "object",
344
- properties: {
345
- action: { type: "string", enum: ["search", "research", "compare", "fact_check", "goap"], description: "Research action type", default: "search" },
346
- query: { type: "string", description: "Search query or topic" },
347
- items: { type: "array", items: { type: "string" }, description: "Items to compare (for 'compare')" },
348
- claim: { type: "string", description: "Claim to verify (for 'fact_check')" },
349
- verify: { type: "boolean", description: "Verify results in goap mode", default: true },
350
- },
351
- required: ["query"],
352
- },
353
- },
354
- {
355
- name: "guidance",
356
- description: "Get instructions on how to use the available tool groups and services. Call this FIRST when unsure which tool to use, when a user asks 'what can you do?', or when you need to understand a specific tool group. Returns structured guidance for the AI on tool selection and usage patterns.",
357
- inputSchema: {
358
- type: "object",
359
- properties: {
360
- topic: {
361
- type: "string",
362
- enum: ["overview", "groups", "intelligence", "agents", "memory", "devtools", "security", "browser", "neural", "agentic-flow", "claude-code", "gemini", "codex", "tool"],
363
- description: "What to get guidance on. Use 'overview' for capabilities summary, 'groups' to see all tool groups and their status, or a specific group name for detailed usage instructions.",
364
- default: "overview",
365
- },
366
- tool_name: { type: "string", description: "Specific tool name to get detailed usage info (when topic='tool')" },
367
- },
368
- },
369
- },
370
- ];
371
-
372
- // =============================================================================
373
- // GUIDANCE ENGINE — AI-facing instruction system
374
- // =============================================================================
375
-
376
- function getGuidance(topic, toolName) {
377
- const activeGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled);
378
- const inactiveGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => !g.enabled);
379
- const externalTools = getActiveTools();
380
-
381
- if (topic === "overview") {
382
- return {
383
- guidance: `# Tool Capabilities Overview
384
-
385
- You have access to ${BUILTIN_TOOLS.length + externalTools.length} tools organized into ${activeGroups.length} active groups.
386
-
387
- ## Active Groups
388
- ${activeGroups.map(([name, g]) => {
389
- const count = name === "core" ? BUILTIN_TOOLS.length : externalTools.filter(t => t.name.startsWith(g.source + "__")).length;
390
- return `- **${name}** (${count} tools) — ${g.description}`;
391
- }).join("\n")}
392
-
393
- ## Inactive Groups (can be enabled)
394
- ${inactiveGroups.map(([name, g]) => `- **${name}** — ${g.description}`).join("\n") || "None"}
395
-
396
- ## Quick Decision Guide
397
- - **Knowledge questions** → use \`search\` first, then \`web_research\` if needed
398
- - **Current events / facts** → use \`web_research\` with action 'search' or 'goap'
399
- - **Complex research** → use \`web_research\` with action 'goap' (multi-step pipeline)
400
- - **"What can you do?"** → call \`guidance\` with topic 'groups'
401
- - **Memory / recall** → use tools from the \`memory\` group
402
- - **Agent orchestration** → use tools from the \`agents\` group
403
- - **Code analysis / performance** → use tools from the \`devtools\` group
404
-
405
- ## Rules
406
- 1. Call tools FIRST, then present results conversationally
407
- 2. Never show raw JSON — synthesize results naturally
408
- 3. For complex questions, prefer GOAP pipeline (web_research action='goap')
409
- 4. Call \`guidance\` with a specific group name to learn how to use that group's tools`,
410
- topic: "overview",
411
- };
412
- }
413
-
414
- if (topic === "groups") {
415
- const groupList = Object.entries(TOOL_GROUPS).map(([name, g]) => {
416
- const status = g.enabled ? "ACTIVE" : "INACTIVE";
417
- const toolCount = name === "core" ? BUILTIN_TOOLS.length :
418
- externalTools.filter(t => {
419
- const backend = t._backend;
420
- return g.source === backend && (!g.prefixes || g.prefixes.some(p => t._originalName.startsWith(p)));
421
- }).length;
422
- return `| ${name} | ${status} | ${toolCount} | ${g.description} |`;
423
- });
424
-
425
- return {
426
- guidance: `# Tool Groups\n\n| Group | Status | Tools | Description |\n|-------|--------|-------|-------------|\n${groupList.join("\n")}`,
427
- topic: "groups",
428
- };
429
- }
430
-
431
- // Specific group guidance
432
- const groupGuides = {
433
- intelligence: `# Intelligence Group (ruvector)
434
-
435
- Self-learning intelligence tools for routing and vector memory.
436
-
437
- ## Key Tools
438
- - **ruvector__hooks_route** — Route a task to the best agent type. Call with a task description.
439
- - **ruvector__hooks_remember** — Store context/knowledge in vector memory for later recall.
440
- - **ruvector__hooks_recall** — Search vector memory semantically. Good for finding past context.
441
- - **ruvector__hooks_pretrain** — Bootstrap intelligence from a code repository.
442
- - **ruvector__hooks_build_agents** — Generate optimized agent configurations.
443
- - **ruvector__hooks_stats** — Get intelligence statistics and learning metrics.
444
-
445
- ## When to Use
446
- - Before starting complex tasks: route to find the best agent approach
447
- - To store important findings for cross-session memory
448
- - To recall previously stored patterns or solutions`,
449
-
450
- agents: `# Agents & Orchestration Group (ruflo)
451
-
452
- Multi-agent lifecycle management, swarm coordination, and task workflows.
453
-
454
- ## Key Tools
455
- - **ruflo__agent_spawn** — Create a new agent with specific capabilities
456
- - **ruflo__agent_list** — List all active agents
457
- - **ruflo__swarm_init** — Initialize a swarm with a topology (mesh, hierarchical, ring, star)
458
- - **ruflo__task_create** — Create and assign tasks
459
- - **ruflo__workflow_create** — Define multi-step workflows
460
- - **ruflo__workflow_execute** — Execute a workflow
461
- - **ruflo__hive-mind_init** — Start collective intelligence coordination
462
- - **ruflo__coordination_orchestrate** — Multi-agent coordination
463
-
464
- ## When to Use
465
- - Complex tasks requiring multiple agents working together
466
- - Pipeline workflows with sequential or parallel steps
467
- - Distributed task management`,
468
-
469
- memory: `# Memory & Knowledge Group (ruflo)
470
-
471
- Vector storage, semantic search, AgentDB pattern learning, and embeddings.
472
-
473
- ## Key Tools
474
- - **ruflo__memory_store** — Store a value with vector embedding for semantic search
475
- - **ruflo__memory_search** — Semantic search across stored memories (HNSW-indexed)
476
- - **ruflo__memory_list** — List stored memory entries
477
- - **ruflo__agentdb_pattern-store** — Store a reasoning pattern for learning
478
- - **ruflo__agentdb_pattern-search** — Search for similar reasoning patterns
479
- - **ruflo__agentdb_context-synthesize** — Synthesize context from stored memories
480
- - **ruflo__embeddings_generate** — Generate vector embeddings for text
481
- - **ruflo__embeddings_search** — Semantic similarity search
482
-
483
- ## When to Use
484
- - Persistent knowledge storage across sessions
485
- - Finding similar past solutions or patterns
486
- - Building semantic search over custom data`,
487
-
488
- devtools: `# Dev Tools Group (ruflo)
489
-
490
- Code analysis, performance profiling, GitHub integration, and terminal access.
491
-
492
- ## Key Tools
493
- - **ruflo__analyze_diff** — Analyze git diff for risk and change classification
494
- - **ruflo__performance_benchmark** — Run performance benchmarks
495
- - **ruflo__performance_bottleneck** — Detect performance bottlenecks
496
- - **ruflo__github_repo_analyze** — Analyze a GitHub repository
497
- - **ruflo__github_pr_manage** — Manage pull requests
498
- - **ruflo__terminal_execute** — Execute commands in a terminal session
499
-
500
- ## When to Use
501
- - Code review and change risk assessment
502
- - Performance analysis and optimization
503
- - GitHub repository management`,
504
-
505
- security: `# Security & Safety Group (ruflo)
506
-
507
- AI defence, PII detection, and claims-based authorization.
508
-
509
- ## Key Tools
510
- - **ruflo__aidefence_scan** — Scan text for AI manipulation attempts
511
- - **ruflo__aidefence_has_pii** — Check for PII (emails, phones, SSNs)
512
- - **ruflo__aidefence_is_safe** — Quick safety check on input
513
- - **ruflo__claims_claim** — Claim an issue for work
514
- - **ruflo__claims_board** — Visual board of all claims
515
-
516
- ## When to Use
517
- - Input validation and safety checking
518
- - PII detection before processing sensitive data
519
- - Work item management across agents`,
520
-
521
- browser: `# Browser Automation Group (ruflo)
522
-
523
- Headless browser control for web interaction and testing.
524
-
525
- ## Key Tools
526
- - **ruflo__browser_open** — Navigate to a URL
527
- - **ruflo__browser_click** — Click elements by reference
528
- - **ruflo__browser_fill** — Fill form inputs
529
- - **ruflo__browser_screenshot** — Capture page screenshots
530
- - **ruflo__browser_snapshot** — Get accessibility tree for AI parsing
531
- - **ruflo__browser_eval** — Execute JavaScript in page context
532
-
533
- ## When to Use
534
- - Web scraping and data extraction
535
- - Automated testing (E2E)
536
- - Form filling and web interaction`,
537
-
538
- neural: `# Neural & DAA Group (ruflo)
539
-
540
- Neural network operations and Decentralized Autonomous Agents.
541
-
542
- ## Key Tools
543
- - **ruflo__neural_train** — Train a neural model
544
- - **ruflo__neural_predict** — Make predictions
545
- - **ruflo__daa_agent_create** — Create an autonomous agent
546
- - **ruflo__daa_workflow_create** — Create autonomous workflows
547
- - **ruflo__daa_knowledge_share** — Share knowledge between agents
548
-
549
- ## When to Use
550
- - Pattern learning and prediction
551
- - Autonomous agent workflows
552
- - Knowledge transfer between agents`,
553
-
554
- "agentic-flow": `# Agentic Flow Group (agentic-flow@alpha)
555
-
556
- Execute 66+ specialized agents with boosted code editing and AgentDB.
557
-
558
- ## Key Tools
559
- - **agentic-flow__agentic_flow_agent** — Execute any of 66+ specialized agents
560
- - **agentic-flow__agentic_flow_list_agents** — List available agent types
561
- - **agentic-flow__agent_booster_edit_file** — 352x faster code editing
562
- - **agentic-flow__agent_booster_batch_edit** — Multi-file refactoring
563
- - **agentic-flow__agentdb_pattern_store** — Store reasoning patterns
564
- - **agentic-flow__agentdb_pattern_search** — Search similar patterns
565
-
566
- ## When to Use
567
- - Complex code generation with specialized agents
568
- - Batch code refactoring across files
569
- - Agent selection when you need the right specialist`,
570
-
571
- "claude-code": `# Claude Code Group
572
-
573
- Anthropic Claude Code MCP server — full coding agent capabilities.
574
-
575
- Requires: ANTHROPIC_API_KEY environment variable.
576
-
577
- ## Capabilities
578
- - File reading and editing
579
- - Bash command execution
580
- - Code analysis and generation
581
- - Project exploration
582
-
583
- ## When to Use
584
- - When you need a second AI perspective on code
585
- - Complex refactoring tasks
586
- - Code review and analysis`,
587
-
588
- gemini: `# Gemini MCP Group
589
-
590
- Google Gemini with conversation context management.
591
-
592
- Requires: GOOGLE_API_KEY environment variable (already set for Gemini models).
593
-
594
- ## Capabilities
595
- - Conversation context management
596
- - Multimodal processing
597
- - Google Search grounding
598
-
599
- ## When to Use
600
- - Extended context conversations
601
- - Multimodal content processing`,
602
-
603
- codex: `# Codex Group
604
-
605
- OpenAI Codex coding agent.
606
-
607
- Requires: OPENAI_API_KEY environment variable (already set for OpenAI models).
608
-
609
- ## Capabilities
610
- - Code generation and execution
611
- - Code completion
612
- - Code explanation
613
-
614
- ## When to Use
615
- - Code generation tasks
616
- - Quick code completions
617
- - Code explanation and documentation`,
618
- };
619
-
620
- if (topic === "tool" && toolName) {
621
- const allTools = [...BUILTIN_TOOLS, ...externalTools];
622
- const tool = allTools.find(t => t.name === toolName);
623
- if (tool) {
624
- const props = Object.entries(tool.inputSchema?.properties || {})
625
- .map(([k, v]) => `- **${k}** (${v.type}) — ${v.description || ""}`)
626
- .join("\n");
627
- return { guidance: `# ${tool.name}\n\n${tool.description}\n\n## Parameters\n${props}`, topic: "tool" };
628
- }
629
- return { guidance: `Tool '${toolName}' not found. Call guidance with topic='groups' to see available tools.`, topic: "tool" };
630
- }
631
-
632
- if (groupGuides[topic]) {
633
- const group = TOOL_GROUPS[topic];
634
- if (!group?.enabled) {
635
- return { guidance: `# ${topic} — INACTIVE\n\n${group?.description || ""}\n\nThis group is not enabled. Set the appropriate MCP_GROUP_* env var to "true" to activate it.`, topic };
636
- }
637
- return { guidance: groupGuides[topic], topic };
638
- }
639
-
640
- return { guidance: `Unknown topic '${topic}'. Use 'overview', 'groups', or a specific group name.`, topic };
641
- }
642
-
643
- // =============================================================================
644
- // SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
645
- // =============================================================================
646
-
647
- const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;
648
-
649
- function assertSafeUrl(rawUrl) {
650
- let parsed;
651
- try {
652
- parsed = new URL(rawUrl);
653
- } catch {
654
- throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
655
- }
656
- if (parsed.protocol !== "https:") {
657
- throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
658
- }
659
- const host = parsed.hostname;
660
- if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
661
- throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
662
- }
663
- }
664
-
665
- // =============================================================================
666
- // HELPER — Call a backend Cloud Function / API
667
- // =============================================================================
668
-
669
- async function callCloudFunction(url, payload, timeoutMs = 25000) {
670
- // Validate the URL before making any network request.
671
- assertSafeUrl(url);
672
- const controller = new AbortController();
673
- const timer = setTimeout(() => controller.abort(), timeoutMs);
674
- try {
675
- const resp = await fetch(url, {
676
- method: "POST",
677
- headers: { "Content-Type": "application/json" },
678
- body: JSON.stringify(payload),
679
- signal: controller.signal,
680
- });
681
- return await resp.json();
682
- } catch (err) {
683
- if (err.name === "AbortError") return { error: "Request timed out", timeout: timeoutMs };
684
- return { error: err.message };
685
- } finally {
686
- clearTimeout(timer);
687
- }
688
- }
689
-
690
- // =============================================================================
691
- // GOAP SEARCH PIPELINE
692
- // =============================================================================
693
-
694
- async function executeGoapSearch(query, args) {
695
- const researchUrl = CLOUD_FUNCTIONS.research;
696
- if (!researchUrl) return { error: "GOAP requires a 'research' URL in CLOUD_FUNCTIONS" };
697
-
698
- const startTime = Date.now();
699
-
700
- const composeResult = await callCloudFunction(researchUrl, {
701
- action: "search",
702
- query: `Break this question into 3-4 distinct search queries that would help answer it comprehensively. Return ONLY the queries, one per line:\n\n${query}`,
703
- }, 30000);
704
-
705
- let searchQueries = [query];
706
- if (composeResult && !composeResult.error) {
707
- const answer = composeResult.result?.answer || composeResult.answer || "";
708
- const lines = answer.split("\n").map(l => l.replace(/^[\d\-\*\.\)]+\s*/, "").trim()).filter(l => l.length > 5 && l.length < 200);
709
- if (lines.length >= 2) searchQueries = lines.slice(0, 4);
710
- }
711
-
712
- const searchResults = await Promise.all(
713
- searchQueries.map(q => callCloudFunction(researchUrl, { action: "search", query: q }, 30000))
714
- );
715
-
716
- const allSources = [], allAnswers = [];
717
- for (let i = 0; i < searchResults.length; i++) {
718
- const r = searchResults[i];
719
- if (r && !r.error && r.success !== false) {
720
- const answer = r.result?.answer || r.answer || "";
721
- if (answer) allAnswers.push({ query: searchQueries[i], answer });
722
- const gm = r.result?.groundingMetadata || r.groundingMetadata || {};
723
- if (gm.sources) allSources.push(...gm.sources);
724
- }
725
- }
726
-
727
- const uniqueSources = [];
728
- const seenUrls = new Set();
729
- for (const src of allSources) {
730
- const url = src.url || src.uri || "";
731
- if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
732
- }
733
-
734
- const synthesisInput = allAnswers.map(a => `## ${a.query}\n${a.answer}`).join("\n\n");
735
- const synthesisResult = await callCloudFunction(researchUrl, {
736
- action: "research",
737
- topic: `Synthesize these findings into a comprehensive answer to: "${query}"\n\nFindings:\n${synthesisInput}`,
738
- }, 60000);
739
-
740
- const synthesizedAnswer = synthesisResult?.result?.answer || synthesisResult?.answer || synthesisInput;
741
- const synthGm = synthesisResult?.result?.groundingMetadata || {};
742
- if (synthGm.sources) {
743
- for (const src of synthGm.sources) {
744
- const url = src.url || src.uri || "";
745
- if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
746
- }
747
- }
748
-
749
- let verification = { verified: true, confidence: "high" };
750
- if (args.verify !== false && synthesizedAnswer.length > 100) {
751
- const vr = await callCloudFunction(researchUrl, {
752
- action: "fact_check", claim: synthesizedAnswer.substring(0, 500),
753
- }, 30000);
754
- if (vr && !vr.error && vr.result) {
755
- verification = {
756
- verified: vr.result.verdict !== "FALSE",
757
- verdict: vr.result.verdict,
758
- confidence: vr.result.confidence || "medium",
759
- details: vr.result.analysis,
760
- };
761
- }
762
- }
763
-
764
- return {
765
- answer: synthesizedAnswer, pipeline: "goap",
766
- steps: { queries_composed: searchQueries.length, searches_executed: searchResults.filter(r => !r?.error).length, sources_found: uniqueSources.length, verification },
767
- sources: uniqueSources.slice(0, 10), searchQueries, duration_ms: Date.now() - startTime,
768
- };
769
- }
770
-
771
- // =============================================================================
772
- // TOOL EXECUTOR
773
- // =============================================================================
774
-
775
- // ADR-166 §6 Phase 1d + 2a — server-side tool gate.
776
- // Enforced HERE (not just in the autopilot handler) so /mcp, /mcp/:group,
777
- // autopilot, and any future path share ONE denial gate. Was the missing
778
- // link that made the disclosed unauthenticated-RCE chain reach shell.
779
- const DANGEROUS_TOOLS = Object.freeze(new Set([
780
- "terminal_execute",
781
- "ruflo__terminal_execute",
782
- "devtools__terminal_execute",
783
- ]));
784
- function isTerminalTool(name) {
785
- return DANGEROUS_TOOLS.has(name) || /terminal_execute/i.test(name);
786
- }
787
- const MCP_ENABLE_TERMINAL = process.env.MCP_ENABLE_TERMINAL === "true";
788
-
789
- async function executeTool(name, args) {
790
- // Deny dangerous tools unless the operator explicitly opted in.
791
- // Enforced on every path (not just autopilot) — root cause of ADR-166 V2/V3.
792
- if (isTerminalTool(name) && !MCP_ENABLE_TERMINAL) {
793
- return {
794
- error:
795
- `Tool "${name}" is disabled by default. Set MCP_ENABLE_TERMINAL=true to allow.`,
796
- code: "TOOL_DISABLED",
797
- };
798
- }
799
- // Validate that search-like tools have a non-empty query to prevent 400 errors
800
- if (!args || typeof args !== "object") args = {};
801
- const rawQuery = args.query ?? args.q ?? args.input ?? "";
802
- const queryStr = typeof rawQuery === "string" ? rawQuery.trim() : String(rawQuery || "").trim();
803
- const isSearchTool = name === "search" || name === "web_research" || /^(web_)?search/i.test(name);
804
- if (isSearchTool && !queryStr) {
805
- return { content: [{ type: "text", text: `No search query provided. Please specify a search query for '${name}'.` }] };
806
- }
807
-
808
- switch (name) {
809
- case "search":
810
- if (!CLOUD_FUNCTIONS.search) return { error: "search endpoint not configured" };
811
- return callCloudFunction(CLOUD_FUNCTIONS.search, { query: args.query, limit: args.limit || 5 });
812
-
813
- case "web_research": {
814
- const action = args.action || "search";
815
- if (action === "goap") return executeGoapSearch(args.query, args);
816
- if (!CLOUD_FUNCTIONS.research) return { error: "research endpoint not configured" };
817
- const payload = { action };
818
- if (action === "search") payload.query = args.query;
819
- else if (action === "research") payload.topic = args.query;
820
- else if (action === "compare") payload.items = args.items;
821
- else if (action === "fact_check") payload.claim = args.claim || args.query;
822
- return callCloudFunction(CLOUD_FUNCTIONS.research, payload, 60000);
823
- }
824
-
825
- case "guidance":
826
- return getGuidance(args.topic || "overview", args.tool_name);
827
-
828
- default: {
829
- // Route to external MCP backend
830
- const activeTools = getActiveTools();
831
- const extTool = activeTools.find(t => t.name === name);
832
- if (extTool) {
833
- const backend = mcpBackends.get(extTool._backend);
834
- if (backend) return backend.callTool(extTool._originalName, args);
835
- return { error: `Backend ${extTool._backend} not available` };
836
- }
837
- return { error: `Unknown tool: ${name}. Call 'guidance' with topic='groups' to see available tools.` };
838
- }
839
- }
840
- }
841
-
842
- // =============================================================================
843
- // PER-GROUP TOOL HELPERS
844
- // =============================================================================
845
-
846
- // Get tools for a specific group only
847
- function getToolsForGroup(groupName) {
848
- const group = TOOL_GROUPS[groupName];
849
- if (!group || !group.enabled) return [];
850
- if (groupName === "core") return BUILTIN_TOOLS;
851
-
852
- const allActive = getActiveTools();
853
- if (!group.prefixes) {
854
- // No prefix filter — return all tools from this backend
855
- return allActive.filter(t => t._backend === group.source);
856
- }
857
- return allActive.filter(t =>
858
- t._backend === group.source && group.prefixes.some(p => t._originalName.startsWith(p))
859
- );
860
- }
861
-
862
- // Group display names for the Chat UI
863
- const GROUP_DISPLAY_NAMES = {
864
- core: "Core Tools",
865
- intelligence: "Intelligence & Learning",
866
- agents: "Agents & Orchestration",
867
- memory: "Memory & Knowledge",
868
- devtools: "Dev Tools & Analysis",
869
- security: "Security & Safety",
870
- browser: "Browser Automation",
871
- neural: "Neural & DAA",
872
- "agentic-flow": "Agentic Flow",
873
- "claude-code": "Claude Code",
874
- gemini: "Gemini",
875
- codex: "Codex",
876
- };
877
-
878
- // =============================================================================
879
- // MCP SERVER — Multiple endpoints per group
880
- // =============================================================================
881
-
882
- const app = express();
883
- app.use(express.json({ limit: "10mb" }));
884
-
885
- // ---------- MCP Streamable HTTP session (#2425 djimit) ----------
886
- // Streamable-HTTP clients (Codex/RMCP) send `DELETE /mcp` with an
887
- // `Mcp-Session-Id` header at shutdown. We echo a stable session id back
888
- // on every /mcp* response so those clients can attach it to the DELETE
889
- // and to `notifications/initialized` handshakes.
890
- const MCP_SESSION_ID = randomUUID();
891
- app.use((req, res, next) => {
892
- if (req.path.startsWith("/mcp")) {
893
- res.setHeader("Mcp-Session-Id", MCP_SESSION_ID);
894
- }
895
- next();
896
- });
897
-
898
- // ---------- CORS middleware (ADR-166 §6 Phase 3b) ----------
899
- const CORS_ALLOWLIST = (process.env.MCP_CORS_ORIGIN || "*")
900
- .split(",").map(s => s.trim()).filter(Boolean);
901
- const CORS_WILDCARD = CORS_ALLOWLIST.length === 1 && CORS_ALLOWLIST[0] === "*";
902
- app.use((req, res, next) => {
903
- const origin = req.get("origin") || "";
904
- if (CORS_WILDCARD) {
905
- res.setHeader("Access-Control-Allow-Origin", "*");
906
- } else if (origin && CORS_ALLOWLIST.includes(origin)) {
907
- res.setHeader("Access-Control-Allow-Origin", origin);
908
- res.setHeader("Vary", "Origin");
909
- }
910
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
911
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
912
- if (req.method === "OPTIONS") return res.sendStatus(204);
913
- next();
914
- });
915
-
916
- // ---------- Auth middleware ----------
917
- // No-op in local-only mode (MCP_AUTH_TOKEN unset). Enforces 401 when token is set.
918
- const MCP_TOKEN = process.env.MCP_AUTH_TOKEN || "";
919
- function requireAuth(req, res, next) {
920
- if (req.path === "/health") return next();
921
- if (!MCP_TOKEN) return next();
922
- const expected = `Bearer ${MCP_TOKEN}`;
923
- const got = req.get("authorization") || "";
924
- const ok = got.length === expected.length &&
925
- timingSafeEqual(Buffer.from(got), Buffer.from(expected));
926
- if (!ok) return res.status(401).json({ error: "unauthorized" });
927
- next();
928
- }
929
- app.use(requireAuth);
930
-
931
- // ---------- Shared MCP handler ----------
932
- function createMcpHandler(groupName) {
933
- return async (req, res) => {
934
- const { method, id, params } = req.body;
935
- try {
936
- switch (method) {
937
- case "initialize":
938
- return res.json({
939
- jsonrpc: "2.0", id,
940
- result: {
941
- protocolVersion: "2024-11-05",
942
- capabilities: { tools: {} },
943
- serverInfo: { name: `mcp-bridge/${groupName}`, version: "2.0.0" },
944
- },
945
- });
946
- case "tools/list": {
947
- const tools = getToolsForGroup(groupName);
948
- return res.json({ jsonrpc: "2.0", id, result: { tools } });
949
- }
950
- case "tools/call": {
951
- const { name, arguments: toolArgs } = params;
952
- const result = await executeTool(name, toolArgs || {});
953
- // If executeTool already returned MCP-formatted content, pass through directly
954
- const mcpResult = result && Array.isArray(result.content)
955
- ? { content: result.content }
956
- : { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
957
- return res.json({ jsonrpc: "2.0", id, result: mcpResult });
958
- }
959
- case "notifications/initialized":
960
- // MCP streamable-HTTP spec: notifications must return 202 Accepted
961
- // with an empty body (no jsonrpc envelope).
962
- return res.status(202).end();
963
- default:
964
- return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
965
- }
966
- } catch (err) {
967
- console.error(`MCP error [${groupName}/${method}]:`, err);
968
- return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
969
- }
970
- };
971
- }
972
-
973
- function createMcpSseHandler(groupName) {
974
- return (req, res) => {
975
- res.setHeader("Content-Type", "text/event-stream");
976
- res.setHeader("Cache-Control", "no-cache");
977
- res.setHeader("Connection", "keep-alive");
978
- res.write(`data: ${JSON.stringify({ type: "endpoint", url: `/mcp/${groupName}` })}\n\n`);
979
- };
980
- }
981
-
982
- // ---------- Register per-group endpoints ----------
983
- for (const groupName of Object.keys(TOOL_GROUPS)) {
984
- app.post(`/mcp/${groupName}`, createMcpHandler(groupName));
985
- app.get(`/mcp/${groupName}`, createMcpSseHandler(groupName));
986
- // #2425 djimit — streamable-HTTP session cleanup
987
- app.delete(`/mcp/${groupName}`, (_, res) => res.sendStatus(204));
988
- }
989
-
990
- // ---------- Catch-all /mcp — serves ALL enabled tools (backwards-compatible) ----------
991
- app.post("/mcp", async (req, res) => {
992
- const { method, id, params } = req.body;
993
- try {
994
- switch (method) {
995
- case "initialize":
996
- return res.json({
997
- jsonrpc: "2.0", id,
998
- result: {
999
- protocolVersion: "2024-11-05",
1000
- capabilities: { tools: {} },
1001
- serverInfo: { name: "mcp-bridge", version: "2.0.0" },
1002
- },
1003
- });
1004
- case "tools/list": {
1005
- const activeTools = getActiveTools();
1006
- return res.json({ jsonrpc: "2.0", id, result: { tools: [...BUILTIN_TOOLS, ...activeTools] } });
1007
- }
1008
- case "tools/call": {
1009
- const { name, arguments: toolArgs } = params;
1010
- const result = await executeTool(name, toolArgs || {});
1011
- // If executeTool already returned MCP-formatted content, pass through directly
1012
- const mcpResult = result && Array.isArray(result.content)
1013
- ? { content: result.content }
1014
- : { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
1015
- return res.json({ jsonrpc: "2.0", id, result: mcpResult });
1016
- }
1017
- case "notifications/initialized":
1018
- return res.status(202).end();
1019
- default:
1020
- return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
1021
- }
1022
- } catch (err) {
1023
- console.error(`MCP error [${method}]:`, err);
1024
- return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
1025
- }
1026
- });
1027
-
1028
- app.get("/mcp", (req, res) => {
1029
- res.setHeader("Content-Type", "text/event-stream");
1030
- res.setHeader("Cache-Control", "no-cache");
1031
- res.setHeader("Connection", "keep-alive");
1032
- res.write(`data: ${JSON.stringify({ type: "endpoint", url: "/mcp" })}\n\n`);
1033
- });
1034
-
1035
- // #2425 djimit — streamable-HTTP session cleanup on the catch-all route.
1036
- app.delete("/mcp", (_, res) => res.sendStatus(204));
1037
-
1038
- // ---------- GET /mcp-servers — returns MCP_SERVERS JSON for Chat UI config ----------
1039
- app.get("/mcp-servers", (_, res) => {
1040
- const servers = [];
1041
- for (const [name, group] of Object.entries(TOOL_GROUPS)) {
1042
- if (!group.enabled) continue;
1043
- const tools = getToolsForGroup(name);
1044
- if (tools.length === 0) continue;
1045
- servers.push({
1046
- name: GROUP_DISPLAY_NAMES[name] || name,
1047
- url: `/mcp/${name}`,
1048
- tools: tools.length,
1049
- group: name,
1050
- });
1051
- }
1052
- res.json(servers);
1053
- });
1054
-
1055
- // =============================================================================
1056
- // CHAT COMPLETIONS PROXY
1057
- // =============================================================================
1058
-
1059
- const PROVIDER_ROUTES = {
1060
- openai: { baseURL: "https://api.openai.com/v1/chat/completions", getKey: () => process.env.OPENAI_API_KEY },
1061
- gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", getKey: () => process.env.GOOGLE_API_KEY },
1062
- openrouter: { baseURL: "https://openrouter.ai/api/v1/chat/completions", getKey: () => process.env.OPENROUTER_API_KEY },
1063
- };
1064
-
1065
- function resolveProvider(model) {
1066
- if (typeof model === "string") {
1067
- if (model.startsWith("gemini-")) return "gemini";
1068
- if (model.includes("/")) return "openrouter";
1069
- }
1070
- return "openai";
1071
- }
1072
-
1073
- // =============================================================================
1074
- // SYSTEM PROMPT — Injected server-side into every chat completion request
1075
- // =============================================================================
1076
- // This comprehensive prompt teaches the AI how to use all 200+ MCP tools
1077
- // across 5 groups. It is injected as the first system message, ensuring
1078
- // consistent behavior regardless of what preprompt the Chat UI sends.
1079
-
1080
- function buildSystemPrompt() {
1081
- // Build dynamic group status
1082
- const enabledGroups = Object.entries(TOOL_GROUPS)
1083
- .filter(([, g]) => g.enabled)
1084
- .map(([name]) => name);
1085
-
1086
- return `You are an intelligent AI assistant with powerful tools organized into ${enabledGroups.length} active groups. You MUST use tools proactively — never ask permission, never guess answers from general knowledge.
1087
-
1088
- IMPORTANT: Call \`guidance\` with topic='overview' if you are ever unsure which tool to use.
1089
-
1090
- # Tool Groups
1091
-
1092
- Your tools are organized into groups. Each tool name is prefixed with its backend (e.g., \`ruflo__agent_spawn\`, \`ruvector__hooks_route\`). Always use the full prefixed name when calling tools.
1093
-
1094
- ## Group 1: Core Tools (always on)
1095
- Built-in tools available in every conversation.
1096
-
1097
- - **search** — Search the knowledge base for documents, procedures, how-tos.
1098
- ALWAYS search before answering knowledge questions — never answer from general knowledge alone.
1099
- - **web_research** — Web search, deep research, comparisons, fact-checking.
1100
- Actions: \`search\` (quick), \`research\` (deep report), \`compare\` (side-by-side), \`fact_check\` (verify), \`goap\` (comprehensive multi-step — BEST for important questions)
1101
- The GOAP pipeline automatically decomposes questions into 3-4 parallel searches, synthesizes findings, and verifies accuracy.
1102
- - **guidance** — Get help on any tool group, specific tool usage, or capabilities overview.
1103
- Topics: \`overview\`, \`groups\`, \`agents\`, \`memory\`, \`intelligence\`, \`devtools\`
1104
- For specific tool help: \`guidance(topic='tool', tool_name='ruflo__agent_spawn')\`
1105
-
1106
- ## Group 2: Intelligence & Learning (ruvector)
1107
- Pattern learning, routing, code analysis, and trajectory tracking. ${TOOL_GROUPS.intelligence.enabled ? "ACTIVE" : "DISABLED"}
1108
-
1109
- ### Essential Intelligence Tools:
1110
- - **ruvector__hooks_route** — Route a task to the optimal agent type. Call this FIRST for complex tasks.
1111
- \`{"task": "describe what needs to be done", "context": ["relevant info"]}\`
1112
- Returns ranked agent recommendations with confidence scores.
1113
- - **ruvector__hooks_remember** — Store a key-value pair in persistent memory for cross-session recall.
1114
- \`{"key": "pattern-name", "value": "what to remember", "namespace": "patterns"}\`
1115
- - **ruvector__hooks_recall** — Retrieve a previously stored memory by key.
1116
- - **ruvector__hooks_suggest_context** — Get contextual suggestions based on current work.
1117
- - **ruvector__hooks_swarm_recommend** — Get swarm topology recommendation for a task type.
1118
- - **ruvector__hooks_capabilities** — List all intelligence system capabilities.
1119
-
1120
- ### Code Analysis:
1121
- - **ruvector__hooks_ast_analyze** — Analyze code structure (AST) of a file.
1122
- - **ruvector__hooks_ast_complexity** — Get complexity metrics for code.
1123
- - **ruvector__hooks_security_scan** — Scan code for security vulnerabilities.
1124
- - **ruvector__hooks_diff_analyze** — Analyze a code diff for risk and impact.
1125
- - **ruvector__hooks_diff_similar** — Find similar past diffs/changes.
1126
-
1127
- ### Trajectory Learning (for multi-step tasks):
1128
- - **ruvector__hooks_trajectory_begin** — Start tracking a multi-step task for learning.
1129
- - **ruvector__hooks_trajectory_step** — Record a step in the current trajectory.
1130
- - **ruvector__hooks_trajectory_end** — End trajectory, triggering pattern extraction.
1131
-
1132
- ### Memory & Compression:
1133
- - **ruvector__hooks_compress** — Compress/summarize long text for efficient storage.
1134
- - **ruvector__hooks_rag_context** — Get RAG context for a query from stored knowledge.
1135
- - **ruvector__hooks_learn** — Force the system to learn from provided examples.
1136
- - **ruvector__hooks_batch_learn** — Learn from multiple examples at once.
1137
- - **ruvector__hooks_stats** — View learning statistics and metrics.
1138
- - **ruvector__hooks_doctor** — Run diagnostics on the intelligence system.
1139
-
1140
- ## Group 3: Agents & Orchestration (ruflo)
1141
- Spawn agents, coordinate swarms, manage tasks and workflows. ${TOOL_GROUPS.agents.enabled ? "ACTIVE" : "DISABLED"}
1142
-
1143
- ### Agent Lifecycle:
1144
- - **ruflo__agent_spawn** — Create a new specialized agent.
1145
- \`{"type": "coder|researcher|tester|reviewer|architect|security", "name": "optional-name"}\`
1146
- Agent types and when to use them:
1147
- - \`coder\` — Write code, implement features, fix bugs
1148
- - \`researcher\` — Find information, analyze documentation, investigate
1149
- - \`tester\` — Write tests, run test suites, validate behavior
1150
- - \`reviewer\` — Review code quality, security, best practices
1151
- - \`architect\` — Design systems, plan architectures, evaluate trade-offs
1152
- - \`security\` — Audit security, find vulnerabilities, recommend fixes
1153
- - **ruflo__agent_status** — Check an agent's current state. \`{"agentId": "agent-xxx"}\`
1154
- - **ruflo__agent_list** — List all active agents with their states.
1155
- - **ruflo__agent_terminate** — Stop an agent. \`{"agentId": "agent-xxx"}\`
1156
- - **ruflo__agent_health** — Health check across all agents.
1157
- - **ruflo__agent_pool** — View the agent pool and available capacity.
1158
-
1159
- ### Swarm Coordination:
1160
- - **ruflo__swarm_init** — Initialize a multi-agent swarm.
1161
- \`{"topology": "hierarchical|mesh|ring|star", "maxAgents": 8, "strategy": "balanced|specialized|adaptive"}\`
1162
- - \`hierarchical\` — Coordinator + workers, best for structured tasks (anti-drift)
1163
- - \`mesh\` — Peer-to-peer, best for collaborative work
1164
- - \`ring\` — Sequential pipeline, best for ordered processing
1165
- - \`star\` — Central hub, best for fan-out parallel work
1166
- - **ruflo__swarm_status** — Get swarm health, topology, and agent states.
1167
- - **ruflo__swarm_health** — Detailed health metrics for the swarm.
1168
- - **ruflo__swarm_shutdown** — Tear down a swarm and all its agents.
1169
-
1170
- ### Task Management:
1171
- - **ruflo__task_create** — Create a tracked task.
1172
- \`{"description": "what needs to be done", "priority": "low|normal|high|critical"}\`
1173
- - **ruflo__task_status** — Check task progress. \`{"taskId": "task-xxx"}\`
1174
- - **ruflo__task_list** — List all tasks with their statuses.
1175
- - **ruflo__task_complete** — Mark a task as done. \`{"taskId": "task-xxx"}\`
1176
- - **ruflo__task_update** — Update task details, status, or assignment.
1177
- - **ruflo__task_cancel** — Cancel a task.
1178
-
1179
- ### Workflow Orchestration:
1180
- - **ruflo__workflow_create** — Define a multi-step workflow with dependencies.
1181
- - **ruflo__workflow_execute** — Run a workflow. \`{"workflowId": "wf-xxx"}\`
1182
- - **ruflo__workflow_status** — Check workflow progress.
1183
- - **ruflo__workflow_template** — Use a pre-built workflow template.
1184
- - **ruflo__workflow_pause** / **ruflo__workflow_resume** — Control workflow execution.
1185
-
1186
- ### Hive-Mind (Distributed Consensus):
1187
- - **ruflo__hive-mind_init** — Start distributed consensus system.
1188
- - **ruflo__hive-mind_spawn** — Add an agent to the hive.
1189
- - **ruflo__hive-mind_consensus** — Run consensus vote across agents.
1190
- - **ruflo__hive-mind_broadcast** — Send message to all hive agents.
1191
- - **ruflo__hive-mind_memory** — Access shared hive memory.
1192
-
1193
- ### Coordination:
1194
- - **ruflo__coordination_topology** — View/change coordination topology.
1195
- - **ruflo__coordination_load_balance** — Distribute work across agents.
1196
- - **ruflo__coordination_orchestrate** — Orchestrate complex multi-agent tasks.
1197
- - **ruflo__coordination_sync** — Synchronize state across agents.
1198
-
1199
- ### Session Management:
1200
- - **ruflo__session_save** — Save current session state.
1201
- - **ruflo__session_restore** — Restore a previous session.
1202
- - **ruflo__session_list** — List available sessions.
1203
-
1204
- ## Group 4: Memory & Knowledge (ruflo)
1205
- Persistent memory, vector search, embeddings, and pattern storage. ${TOOL_GROUPS.memory.enabled ? "ACTIVE" : "DISABLED"}
1206
-
1207
- ### Memory Operations:
1208
- - **ruflo__memory_store** — Store data in persistent memory.
1209
- \`{"key": "my-key", "value": "data to store", "namespace": "default", "tags": ["tag1"]}\`
1210
- - **ruflo__memory_retrieve** — Get stored data by key. \`{"key": "my-key"}\`
1211
- - **ruflo__memory_search** — Semantic vector search across stored memories.
1212
- \`{"query": "what to search for", "limit": 5, "namespace": "default"}\`
1213
- - **ruflo__memory_list** — List all stored keys in a namespace.
1214
- - **ruflo__memory_delete** — Remove a stored memory.
1215
- - **ruflo__memory_stats** — View memory usage statistics.
1216
-
1217
- ### Embeddings:
1218
- - **ruflo__embeddings_generate** — Generate vector embeddings for text.
1219
- - **ruflo__embeddings_compare** — Compare semantic similarity of two texts.
1220
- - **ruflo__embeddings_search** — Search embeddings database by similarity.
1221
- - **ruflo__embeddings_neural** — Generate neural embeddings.
1222
- - **ruflo__embeddings_hyperbolic** — Generate hyperbolic embeddings for hierarchical data.
1223
-
1224
- ### AgentDB (Advanced Pattern Storage):
1225
- - **ruflo__agentdb_pattern-store** — Store a learned pattern with metadata.
1226
- \`{"pattern": "description", "category": "code|debug|architecture", "confidence": 0.9}\`
1227
- - **ruflo__agentdb_pattern-search** — Search patterns by similarity.
1228
- - **ruflo__agentdb_route** — Route a query to the most relevant stored pattern.
1229
- - **ruflo__agentdb_feedback** — Provide feedback on a pattern (reinforcement learning).
1230
- - **ruflo__agentdb_context-synthesize** — Synthesize context from multiple sources.
1231
- - **ruflo__agentdb_semantic-route** — Semantic routing based on stored knowledge.
1232
- - **ruflo__agentdb_consolidate** — Consolidate and deduplicate stored patterns.
1233
- - **ruflo__agentdb_batch** — Batch operations on patterns.
1234
- - **ruflo__agentdb_session-start** / **ruflo__agentdb_session-end** — Session tracking.
1235
- - **ruflo__agentdb_hierarchical-store** / **ruflo__agentdb_hierarchical-recall** — Hierarchical memory.
1236
-
1237
- ## Group 5: Dev Tools & Analysis (ruflo)
1238
- Performance, system health, GitHub integration, code analysis, terminal. ${TOOL_GROUPS.devtools.enabled ? "ACTIVE" : "DISABLED"}
1239
-
1240
- ### System & Performance:
1241
- - **ruflo__system_status** — System health overview.
1242
- - **ruflo__system_metrics** — Detailed performance metrics.
1243
- - **ruflo__system_health** — Health check across all subsystems.
1244
- - **ruflo__performance_report** — Generate performance report.
1245
- - **ruflo__performance_bottleneck** — Identify performance bottlenecks.
1246
- - **ruflo__performance_benchmark** — Run benchmarks.
1247
- - **ruflo__performance_optimize** — Get optimization recommendations.
1248
- - **ruflo__performance_profile** — Profile specific operations.
1249
-
1250
- ### Code Analysis:
1251
- - **ruflo__analyze_diff** — Analyze a code diff.
1252
- - **ruflo__analyze_diff-risk** — Assess risk level of changes.
1253
- - **ruflo__analyze_diff-classify** — Classify type of changes (feature, bugfix, refactor).
1254
- - **ruflo__analyze_diff-reviewers** — Suggest code reviewers.
1255
- - **ruflo__analyze_file-risk** — Assess risk of a specific file.
1256
-
1257
- ### GitHub Integration:
1258
- - **ruflo__github_repo_analyze** — Analyze a GitHub repository.
1259
- \`{"repo": "owner/repo", "analysis_type": "code_quality|performance|security"}\`
1260
- - **ruflo__github_pr_manage** — Manage pull requests (create, review, merge).
1261
- - **ruflo__github_issue_track** — Track and manage issues.
1262
- - **ruflo__github_workflow** — Manage GitHub Actions workflows.
1263
- - **ruflo__github_metrics** — Repository metrics and insights.
1264
-
1265
- ### Terminal Access:
1266
- - **ruflo__terminal_create** — Create a terminal session.
1267
- - **ruflo__terminal_execute** — Execute a command. \`{"command": "ls -la"}\`
1268
- - **ruflo__terminal_list** — List active terminals.
1269
- - **ruflo__terminal_history** — View command history.
1270
-
1271
- ### Development Hooks:
1272
- - **ruflo__hooks_pre-task** / **ruflo__hooks_post-task** — Task lifecycle hooks for learning.
1273
- - **ruflo__hooks_pre-edit** / **ruflo__hooks_post-edit** — File edit hooks.
1274
- - **ruflo__hooks_session-start** / **ruflo__hooks_session-end** — Session lifecycle.
1275
- - **ruflo__hooks_worker-dispatch** — Dispatch background workers.
1276
- Workers: \`optimize\`, \`audit\`, \`testgaps\`, \`document\`, \`map\`, \`deepdive\`, \`benchmark\`
1277
- - **ruflo__hooks_model-route** — Route to optimal AI model for a task.
1278
- - **ruflo__hooks_explain** — Explain a routing or intelligence decision.
1279
-
1280
- ### Configuration:
1281
- - **ruflo__config_get** / **ruflo__config_set** / **ruflo__config_list** — Manage settings.
1282
-
1283
- ### Progress Tracking:
1284
- - **ruflo__progress_check** — Check implementation progress.
1285
- - **ruflo__progress_summary** — Summarize overall progress.
1286
-
1287
- # Decision Framework
1288
-
1289
- When the user asks you something, follow this decision tree:
1290
-
1291
- 1. **Knowledge question** ("how do I...", "what is...") → \`search\` first, then \`web_research\` if not found
1292
- 2. **Research request** ("look up", "compare", "find out") → \`web_research\` with appropriate action (use \`goap\` for important questions)
1293
- 3. **Code task** ("write", "fix", "implement") → \`ruvector__hooks_route\` to find best approach, then \`ruflo__agent_spawn\`
1294
- 4. **Analysis request** ("analyze", "review", "audit") → spawn reviewer/security agents + analysis tools
1295
- 5. **Multi-step project** → \`ruflo__task_create\` for tracking, \`ruflo__swarm_init\` for coordination
1296
- 6. **Memory/recall** ("remember", "save", "what did we...") → \`ruflo__memory_store\` / \`ruflo__memory_search\`
1297
- 7. **System question** ("what tools", "help") → \`guidance(topic='overview')\`
1298
- 8. **Performance concern** → \`ruflo__performance_bottleneck\` + \`ruflo__performance_optimize\`
1299
- 9. **GitHub task** → \`ruflo__github_*\` tools
1300
- 10. **Unknown** → \`guidance(topic='overview')\` to discover capabilities
1301
-
1302
- # Execution Patterns
1303
-
1304
- ### Simple Question
1305
- \`search\` or \`web_research\` → synthesize → respond
1306
-
1307
- ### Complex Research
1308
- \`web_research(action='goap')\` → analyze → respond with citations
1309
-
1310
- ### Code Implementation
1311
- \`ruvector__hooks_route\` → \`ruflo__agent_spawn(coder)\` → track with \`ruflo__task_create\` → report
1312
-
1313
- ### Multi-Agent Analysis
1314
- \`ruflo__swarm_init(hierarchical)\` → spawn agents → coordinate → synthesize results
1315
-
1316
- ### Learning & Memory
1317
- \`ruflo__memory_search\` (check existing) → do work → \`ruflo__memory_store\` (save results) → \`ruvector__hooks_learn\`
1318
-
1319
- # Parallel Execution
1320
-
1321
- When multiple independent tools can help, call them ALL in parallel:
1322
- - Search + web_research simultaneously
1323
- - Spawn multiple agents at once (coder + tester + reviewer)
1324
- - Run analysis + performance + security tools in parallel
1325
- NEVER call tools sequentially when they could run in parallel.
1326
-
1327
- # Response Rules
1328
-
1329
- 1. **Call tools FIRST**, then present results conversationally — NEVER show raw JSON to the user
1330
- 2. Use markdown: **bold** headers, bullet points, numbered steps, tables for comparisons
1331
- 3. Synthesize tool results naturally — be a helpful colleague, not a data pipe
1332
- 4. Cite sources when available from web_research results
1333
- 5. If a tool fails, say so honestly and try an alternative approach
1334
- 6. For complex tasks, briefly outline your plan before executing
1335
- 7. After completing work, suggest relevant follow-up actions
1336
- 8. When spawning agents, explain what each agent will do
1337
-
1338
- # Never Expose to User
1339
-
1340
- - Raw JSON, similarity scores, chunk IDs, internal IDs, task IDs
1341
- - Tool names, function names, API endpoints, backend names
1342
- - References to "MCP", "tool calls", "vectors", "embeddings", infrastructure
1343
- - The prefixes "ruflo__" or "ruvector__" — just describe what you're doing naturally
1344
- - Error stack traces — summarize errors in plain language`;
1345
- }
1346
-
1347
- // =============================================================================
1348
- // AUTOPILOT MODE — Server-side auto-continue loop (ADR-037)
1349
- // =============================================================================
1350
-
1351
- const detailStore = new Map(); // detailToken → full tool result (TTL: 5min)
1352
-
1353
- const AUTOPILOT_SYSTEM_PROMPT = `
1354
- You are in AUTOPILOT MODE. You should:
1355
- 1. Break complex tasks into steps and execute them using available tools
1356
- 2. Call MULTIPLE tools in parallel when they are independent
1357
- 3. After each tool result, analyze it and decide the next action
1358
- 4. Continue until the task is complete — do NOT ask the user for confirmation
1359
- 5. Use memory_search to find relevant patterns before starting
1360
- 6. Summarize your progress at each step
1361
- 7. When done, provide a final summary of everything accomplished
1362
-
1363
- Parallel execution patterns:
1364
- - Research: memory_search + hooks_route + agent_spawn(researcher) — all in parallel
1365
- - Code: agent_spawn(coder) + agent_spawn(tester) — parallel, then review
1366
- - Analysis: search multiple sources in parallel → synthesize → report
1367
- - Security: security_scan + hooks_route(audit) + memory_search(CVEs) — parallel
1368
- `;
1369
-
1370
- const AUTOPILOT_BLOCKED_PATTERNS = [
1371
- /^deploy_/,
1372
- /^security_delete/,
1373
- /^browser_fill$/,
1374
- /^browser_click$/,
1375
- /terminal_execute/,
1376
- ];
1377
-
1378
- function isBlockedTool(name) {
1379
- return AUTOPILOT_BLOCKED_PATTERNS.some(p => p.test(name));
1380
- }
1381
-
1382
- function sendAutopilotEvent(res, data) {
1383
- res.write(`data: ${JSON.stringify(data)}\n\n`);
1384
- }
1385
-
1386
- function safeParseArgs(args) {
1387
- if (typeof args === 'object' && args !== null) return args;
1388
- try { return JSON.parse(args || '{}'); } catch { return {}; }
1389
- }
1390
-
1391
- function autopilotSleep(ms) {
1392
- return new Promise(resolve => setTimeout(resolve, ms));
1393
- }
1394
-
1395
- async function handleAutopilot(req, res, provider, body) {
1396
- const maxSteps = Math.min(parseInt(req.headers['x-autopilot-max-steps'] || '20', 10), 50);
1397
- const cooldownMs = parseInt(process.env.AUTOPILOT_COOLDOWN || '500', 10);
1398
- const stepTimeoutMs = parseInt(process.env.AUTOPILOT_STEP_TIMEOUT || '30000', 10);
1399
-
1400
- // SSE setup
1401
- res.setHeader('Content-Type', 'text/event-stream');
1402
- res.setHeader('Cache-Control', 'no-cache');
1403
- res.setHeader('Connection', 'keep-alive');
1404
- res.setHeader('X-Accel-Buffering', 'no');
1405
-
1406
- let messages = [...body.messages];
1407
- let step = 0;
1408
- let aborted = false;
1409
- let totalTasks = 0;
1410
- const startTime = Date.now();
1411
-
1412
- req.on('close', () => { aborted = true; });
1413
-
1414
- sendAutopilotEvent(res, { type: 'autopilot_start', maxSteps });
1415
-
1416
- // Get the tools list for the AI provider (OpenAI function calling format)
1417
- const allTools = [...BUILTIN_TOOLS, ...getActiveTools()];
1418
- const toolDefs = allTools.map(t => ({
1419
- type: 'function',
1420
- function: {
1421
- name: t.name,
1422
- description: t.description || '',
1423
- parameters: t.inputSchema || { type: 'object', properties: {} },
1424
- },
1425
- }));
1426
-
1427
- while (step < maxSteps && !aborted) {
1428
- // 1. Call upstream AI provider (non-streaming for tool call parsing)
1429
- const apiKey = provider.getKey();
1430
- let aiResult;
1431
- try {
1432
- const aiResponse = await fetch(provider.baseURL, {
1433
- method: 'POST',
1434
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
1435
- body: JSON.stringify({
1436
- ...body,
1437
- messages,
1438
- stream: false,
1439
- tools: toolDefs.length > 0 ? toolDefs : undefined,
1440
- }),
1441
- signal: AbortSignal.timeout(stepTimeoutMs),
1442
- });
1443
- aiResult = await aiResponse.json();
1444
- } catch (err) {
1445
- sendAutopilotEvent(res, { type: 'autopilot_error', error: `AI call failed: ${err.message}` });
1446
- break;
1447
- }
1448
-
1449
- const choice = aiResult.choices?.[0];
1450
- if (!choice) {
1451
- sendAutopilotEvent(res, { type: 'autopilot_error', error: 'No response from AI' });
1452
- break;
1453
- }
1454
-
1455
- // 2. Check for tool calls
1456
- const toolCalls = choice.message?.tool_calls;
1457
-
1458
- if (!toolCalls || toolCalls.length === 0) {
1459
- // Final text response — send it
1460
- sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message?.content || '' });
1461
- break;
1462
- }
1463
-
1464
- // 3. Execute ALL tool calls in parallel
1465
- step++;
1466
- const groupId = `g${step}`;
1467
- const taskEvents = toolCalls.map((tc, i) => ({
1468
- taskId: `t${totalTasks + i + 1}`,
1469
- tool: tc.function.name,
1470
- args: safeParseArgs(tc.function.arguments),
1471
- status: 'running',
1472
- }));
1473
- totalTasks += taskEvents.length;
1474
-
1475
- // If the AI also included text content, stream it before tools
1476
- if (choice.message?.content) {
1477
- sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message.content });
1478
- }
1479
-
1480
- // Stream group start
1481
- sendAutopilotEvent(res, { type: 'task_group_start', groupId, step, tasks: taskEvents });
1482
-
1483
- // Append assistant message to conversation
1484
- messages.push(choice.message);
1485
-
1486
- // Execute tools in parallel
1487
- const groupStart = Date.now();
1488
- const results = await Promise.allSettled(
1489
- toolCalls.map(async (tc, i) => {
1490
- const taskId = taskEvents[i].taskId;
1491
- const toolName = tc.function.name;
1492
- const toolArgs = safeParseArgs(tc.function.arguments);
1493
- const taskStart = Date.now();
1494
-
1495
- // Check blocklist
1496
- if (isBlockedTool(toolName)) {
1497
- sendAutopilotEvent(res, {
1498
- type: 'task_update', taskId, status: 'blocked',
1499
- summary: `${toolName} requires confirmation`,
1500
- duration: Date.now() - taskStart,
1501
- });
1502
- return { toolCallId: tc.id, blocked: true, toolName };
1503
- }
1504
-
1505
- try {
1506
- const result = await executeTool(toolName, toolArgs);
1507
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1508
-
1509
- // Store full detail, generate token for lazy loading
1510
- const detailToken = `dt_${taskId}_${Date.now()}`;
1511
- detailStore.set(detailToken, resultStr);
1512
-
1513
- // Stream task completion with summary only
1514
- const summary = resultStr.length > 120
1515
- ? resultStr.substring(0, 120).replace(/\n/g, ' ') + '...'
1516
- : resultStr.replace(/\n/g, ' ');
1517
-
1518
- sendAutopilotEvent(res, {
1519
- type: 'task_update', taskId, status: 'completed',
1520
- summary, duration: Date.now() - taskStart, detailToken,
1521
- });
1522
-
1523
- return { toolCallId: tc.id, content: resultStr };
1524
- } catch (err) {
1525
- sendAutopilotEvent(res, {
1526
- type: 'task_update', taskId, status: 'failed',
1527
- summary: err.message, duration: Date.now() - taskStart,
1528
- });
1529
- return { toolCallId: tc.id, content: `Error: ${err.message}` };
1530
- }
1531
- })
1532
- );
1533
-
1534
- // Stream group end
1535
- sendAutopilotEvent(res, { type: 'task_group_end', groupId, step, duration: Date.now() - groupStart });
1536
-
1537
- // Check if any tools were blocked — pause autopilot
1538
- const blockedResults = results
1539
- .filter(r => r.status === 'fulfilled' && r.value.blocked)
1540
- .map(r => r.value);
1541
- if (blockedResults.length > 0) {
1542
- sendAutopilotEvent(res, {
1543
- type: 'autopilot_paused',
1544
- reason: 'blocked_tools',
1545
- tools: blockedResults.map(b => b.toolName),
1546
- });
1547
- break;
1548
- }
1549
-
1550
- // Append tool results to messages
1551
- for (const r of results) {
1552
- if (r.status === 'fulfilled' && !r.value.blocked) {
1553
- messages.push({
1554
- role: 'tool',
1555
- tool_call_id: r.value.toolCallId,
1556
- content: r.value.content,
1557
- });
1558
- }
1559
- }
1560
-
1561
- // Cooldown to prevent runaway
1562
- await autopilotSleep(cooldownMs);
1563
- }
1564
-
1565
- if (step >= maxSteps && !aborted) {
1566
- sendAutopilotEvent(res, {
1567
- type: 'autopilot_text',
1568
- content: `\n⚠️ Autopilot reached max steps (${maxSteps}). Stopping.\n`,
1569
- });
1570
- }
1571
-
1572
- sendAutopilotEvent(res, {
1573
- type: 'autopilot_end',
1574
- totalSteps: step,
1575
- totalTasks,
1576
- duration: Date.now() - startTime,
1577
- });
1578
-
1579
- res.write('data: [DONE]\n\n');
1580
- res.end();
1581
-
1582
- // Clean up detail store after 5 minutes
1583
- const detailTTL = parseInt(process.env.AUTOPILOT_DETAIL_TTL || '300000', 10);
1584
- setTimeout(() => {
1585
- for (const [key] of detailStore) {
1586
- if (key.startsWith('dt_')) detailStore.delete(key);
1587
- }
1588
- }, detailTTL);
1589
- }
1590
-
1591
- // Lazy detail loading endpoint
1592
- app.get('/autopilot/detail/:token', (req, res) => {
1593
- const content = detailStore.get(req.params.token);
1594
- if (content) {
1595
- res.json({ content });
1596
- } else {
1597
- res.status(404).json({ error: 'Detail expired or not found' });
1598
- }
1599
- });
1600
-
1601
- // =============================================================================
1602
- // CHAT COMPLETIONS PROXY
1603
- // =============================================================================
1604
-
1605
- app.post("/chat/completions", async (req, res) => {
1606
- const model = req.body?.model;
1607
- const providerName = resolveProvider(model);
1608
- const provider = PROVIDER_ROUTES[providerName];
1609
- const apiKey = provider.getKey();
1610
-
1611
- if (!apiKey) return res.status(401).json({ error: { message: `No API key for provider: ${providerName}` } });
1612
-
1613
- // Inject comprehensive system prompt as the first message
1614
- const body = { ...req.body };
1615
- if (body.messages && Array.isArray(body.messages)) {
1616
- let systemPrompt = buildSystemPrompt();
1617
- // Add autopilot instructions if autopilot mode is active
1618
- const isAutopilot = req.headers['x-autopilot'] === 'true';
1619
- if (isAutopilot) {
1620
- systemPrompt = AUTOPILOT_SYSTEM_PROMPT + '\n\n' + systemPrompt;
1621
- }
1622
- // Prepend our system prompt before any existing messages
1623
- const hasSystemMsg = body.messages[0]?.role === "system";
1624
- if (hasSystemMsg) {
1625
- // Merge with existing system message
1626
- body.messages = [
1627
- { role: "system", content: systemPrompt + "\n\n" + body.messages[0].content },
1628
- ...body.messages.slice(1),
1629
- ];
1630
- } else {
1631
- body.messages = [{ role: "system", content: systemPrompt }, ...body.messages];
1632
- }
1633
- }
1634
-
1635
- // Route to autopilot handler if x-autopilot header is set
1636
- if (req.headers['x-autopilot'] === 'true') {
1637
- return handleAutopilot(req, res, provider, body);
1638
- }
1639
-
1640
- try {
1641
- const upstream = await fetch(provider.baseURL, {
1642
- method: "POST",
1643
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1644
- body: JSON.stringify(body),
1645
- });
1646
-
1647
- if (!upstream.ok) {
1648
- const errBody = await upstream.text();
1649
- console.error(`Proxy error [${providerName}/${model}]: ${upstream.status} ${errBody.substring(0, 200)}`);
1650
- // Normalize all upstream errors into OpenAI-compatible format so the
1651
- // Chat UI's OpenAI SDK can parse them instead of "400 (no body)".
1652
- let errorMessage = `Upstream ${providerName} error (${upstream.status})`;
1653
- try {
1654
- const parsed = JSON.parse(errBody);
1655
- // Gemini returns [{"error": {"message": "..."}}]
1656
- if (Array.isArray(parsed) && parsed[0]?.error?.message) {
1657
- errorMessage = parsed[0].error.message;
1658
- // OpenAI/OpenRouter return {"error": {"message": "..."}}
1659
- } else if (parsed.error?.message) {
1660
- errorMessage = parsed.error.message;
1661
- }
1662
- } catch {}
1663
- return res.status(upstream.status).json({
1664
- error: { message: errorMessage, type: "upstream_error", code: upstream.status },
1665
- });
1666
- }
1667
-
1668
- res.setHeader("Content-Type", upstream.headers.get("content-type") || "application/json");
1669
-
1670
- if (req.body?.stream && upstream.body) {
1671
- const reader = upstream.body.getReader();
1672
- const decoder = new TextDecoder();
1673
- try {
1674
- while (true) {
1675
- const { done, value } = await reader.read();
1676
- if (done) break;
1677
- res.write(decoder.decode(value, { stream: true }));
1678
- }
1679
- } catch (e) { /* stream closed */ }
1680
- finally { res.end(); }
1681
- } else {
1682
- res.send(await upstream.text());
1683
- }
1684
- } catch (err) {
1685
- console.error(`Proxy error [${providerName}/${model}]:`, err.message);
1686
- res.status(502).json({ error: { message: `Upstream error: ${err.message}` } });
1687
- }
1688
- });
1689
-
1690
- // =============================================================================
1691
- // MODELS & HEALTH
1692
- // =============================================================================
1693
-
1694
- const KNOWN_MODELS = [
1695
- "gemini-2.5-pro", "gemini-2.5-flash",
1696
- "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini",
1697
- "o3-mini", "o1-mini",
1698
- ];
1699
-
1700
- app.get("/models", (_, res) => {
1701
- res.json({ object: "list", data: KNOWN_MODELS.map(id => ({ id, object: "model", owned_by: "system" })) });
1702
- });
1703
-
1704
- app.get("/health", (_, res) => {
1705
- const backends = {};
1706
- for (const [name, client] of mcpBackends) {
1707
- backends[name] = { ready: client.ready, tools: client.tools.length };
1708
- }
1709
- const activeTools = getActiveTools();
1710
- const groups = {};
1711
- for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1712
- groups[name] = { enabled: g.enabled, source: g.source };
1713
- }
1714
- res.json({
1715
- status: "ok", service: "mcp-bridge", version: "2.0.0",
1716
- tools: { builtin: BUILTIN_TOOLS.length, external: activeTools.length, total: BUILTIN_TOOLS.length + activeTools.length },
1717
- groups, backends,
1718
- });
1719
- });
1720
-
1721
- // GET /groups — list tool groups and their status
1722
- app.get("/groups", (_, res) => {
1723
- const activeTools = getActiveTools();
1724
- const result = {};
1725
- for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1726
- const tools = name === "core" ? BUILTIN_TOOLS :
1727
- activeTools.filter(t => {
1728
- if (g.source !== t._backend) return false;
1729
- if (!g.prefixes) return true;
1730
- return g.prefixes.some(p => t._originalName.startsWith(p));
1731
- });
1732
- result[name] = {
1733
- enabled: g.enabled,
1734
- description: g.description,
1735
- tools: tools.length,
1736
- toolNames: tools.map(t => t.name).slice(0, 10),
1737
- };
1738
- }
1739
- res.json(result);
1740
- });
1741
-
1742
- // =============================================================================
1743
- // STARTUP
1744
- // =============================================================================
1745
-
1746
- async function main() {
1747
- const isPublic = BIND_HOST !== "127.0.0.1" && BIND_HOST !== "localhost";
1748
- if (isPublic && !process.env.MCP_AUTH_TOKEN) {
1749
- console.error(
1750
- "FATAL: refusing to bind a public interface without MCP_AUTH_TOKEN. " +
1751
- "Generate one with: MCP_AUTH_TOKEN=$(openssl rand -base64 32)"
1752
- );
1753
- process.exit(1);
1754
- }
1755
- app.listen(PORT, BIND_HOST, () => {
1756
- console.log(`MCP Bridge v2.0.0 on port ${PORT} (${BIND_HOST})`);
1757
- const enabled = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled).map(([n]) => n);
1758
- console.log(`Active groups: ${enabled.join(", ")}`);
1759
- // ADR-166 §6 — startup posture banner
1760
- console.log(
1761
- `[security] bind=${BIND_HOST} auth=${process.env.MCP_AUTH_TOKEN ? "bearer" : "off (local-only)"} ` +
1762
- `terminal=${MCP_ENABLE_TERMINAL ? "ENABLED (⚠ opt-in)" : "disabled"}`,
1763
- );
1764
- if (MCP_ENABLE_TERMINAL) {
1765
- console.warn(
1766
- "[security] WARNING: terminal_execute is enabled. This tool grants shell access " +
1767
- "inside the bridge container to any client the auth layer accepts. Ensure " +
1768
- "MCP_AUTH_TOKEN is set on any non-loopback bind. See ADR-166 §6 Phase 1d.",
1769
- );
1770
- }
1771
- });
1772
-
1773
- const anyBackendNeeded = BACKEND_DEFS.some(isBackendNeeded);
1774
- if (anyBackendNeeded) {
1775
- console.log("Initializing MCP backends...");
1776
- await initBackends();
1777
- }
1778
- }
1779
-
1780
- main().catch(err => { console.error("Fatal:", err); process.exit(1); });
1
+ import express from "express";
2
+ import { spawn } from "child_process";
3
+ import { randomUUID, timingSafeEqual } from "crypto";
4
+
5
+ // =============================================================================
6
+ // CONFIGURATION
7
+ // =============================================================================
8
+
9
+ const CLOUD_FUNCTIONS = {
10
+ // search: process.env.SEARCH_API_URL || "https://my-search-api.run.app",
11
+ // research: process.env.RESEARCH_API_URL || "https://my-research-api.run.app",
12
+ };
13
+
14
+ const PORT = parseInt(process.env.PORT || "3001", 10);
15
+ const BIND_HOST = process.env.MCP_BIND_HOST || "127.0.0.1";
16
+
17
+ // =============================================================================
18
+ // TOOL GROUPS — Enable/disable categories of tools independently
19
+ // =============================================================================
20
+ // Groups map tool name prefixes from backends to logical categories.
21
+ // Each group can be toggled via env var. The AI sees only enabled tools.
22
+
23
+ const TOOL_GROUPS = {
24
+ // --- Core (always on, built-in) ---
25
+ core: {
26
+ enabled: true, // cannot be disabled
27
+ description: "Search, research, and guidance tools",
28
+ source: "builtin",
29
+ },
30
+
31
+ // --- Intelligence (ruvector) ---
32
+ intelligence: {
33
+ enabled: process.env.MCP_GROUP_INTELLIGENCE !== "false",
34
+ description: "Self-learning intelligence — routing, memory, pattern training (ruvector)",
35
+ source: "ruvector",
36
+ prefixes: ["hooks_"],
37
+ },
38
+
39
+ // --- Agents & Orchestration (ruflo) ---
40
+ agents: {
41
+ enabled: process.env.MCP_GROUP_AGENTS !== "false",
42
+ description: "Agent lifecycle, swarm coordination, task management, workflows (ruflo)",
43
+ source: "ruflo",
44
+ prefixes: ["agent_", "swarm_", "task_", "session_", "hive-mind_", "workflow_", "coordination_"],
45
+ },
46
+
47
+ // --- Memory & Knowledge (ruflo) ---
48
+ memory: {
49
+ enabled: process.env.MCP_GROUP_MEMORY !== "false",
50
+ description: "Vector memory, AgentDB, embeddings, semantic search (ruflo)",
51
+ source: "ruflo",
52
+ prefixes: ["memory_", "agentdb_", "embeddings_"],
53
+ },
54
+
55
+ // --- Dev Tools (ruflo) ---
56
+ devtools: {
57
+ enabled: process.env.MCP_GROUP_DEVTOOLS !== "false",
58
+ description: "Hooks, code analysis, performance profiling, GitHub integration (ruflo)",
59
+ source: "ruflo",
60
+ prefixes: ["hooks_", "analyze_", "performance_", "github_", "terminal_", "config_", "system_", "progress_"],
61
+ },
62
+
63
+ // --- Security & Safety (ruflo) ---
64
+ security: {
65
+ enabled: process.env.MCP_GROUP_SECURITY === "true",
66
+ description: "AI defence, PII detection, claims management, pattern transfer (ruflo)",
67
+ source: "ruflo",
68
+ prefixes: ["aidefence_", "claims_", "transfer_"],
69
+ },
70
+
71
+ // --- Browser Automation (ruflo) ---
72
+ browser: {
73
+ enabled: process.env.MCP_GROUP_BROWSER === "true",
74
+ description: "Headless browser control — navigate, click, fill, screenshot (ruflo)",
75
+ source: "ruflo",
76
+ prefixes: ["browser_"],
77
+ },
78
+
79
+ // --- Neural & DAA (ruflo) ---
80
+ neural: {
81
+ enabled: process.env.MCP_GROUP_NEURAL === "true",
82
+ description: "Neural network training, DAA autonomous agents, cognitive patterns (ruflo)",
83
+ source: "ruflo",
84
+ prefixes: ["neural_", "daa_"],
85
+ },
86
+
87
+ // --- Agentic Flow (agentic-flow@alpha) ---
88
+ "agentic-flow": {
89
+ enabled: process.env.MCP_GROUP_AGENTIC_FLOW === "true",
90
+ description: "Execute 66+ specialized agents, batch code editing, AgentDB patterns (agentic-flow)",
91
+ source: "agentic-flow",
92
+ prefixes: ["agentic_flow_", "agent_booster_", "agentdb_"],
93
+ },
94
+
95
+ // --- Claude Code ---
96
+ "claude-code": {
97
+ enabled: process.env.MCP_GROUP_CLAUDE_CODE === "true",
98
+ description: "Anthropic Claude Code — file editing, bash execution, code analysis (requires ANTHROPIC_API_KEY)",
99
+ source: "claude",
100
+ },
101
+
102
+ // --- Gemini MCP ---
103
+ gemini: {
104
+ enabled: process.env.MCP_GROUP_GEMINI === "true",
105
+ description: "Google Gemini conversation context, multimodal capabilities (requires GOOGLE_API_KEY)",
106
+ source: "gemini-mcp",
107
+ },
108
+
109
+ // --- OpenAI Codex ---
110
+ codex: {
111
+ enabled: process.env.MCP_GROUP_CODEX === "true",
112
+ description: "OpenAI Codex coding agent — code generation and execution (requires OPENAI_API_KEY)",
113
+ source: "codex",
114
+ },
115
+ };
116
+
117
+ // =============================================================================
118
+ // STDIO MCP CLIENT — Connects to external MCP servers via child process
119
+ // =============================================================================
120
+
121
+ class StdioMcpClient {
122
+ constructor(name, command, args = []) {
123
+ this.name = name;
124
+ this.command = command;
125
+ this.args = args;
126
+ this.process = null;
127
+ this.tools = [];
128
+ this.ready = false;
129
+ this.pending = new Map();
130
+ this.buffer = "";
131
+ }
132
+
133
+ async start() {
134
+ return new Promise((resolve) => {
135
+ try {
136
+ this.process = spawn(this.command, this.args, {
137
+ stdio: ["pipe", "pipe", "pipe"],
138
+ env: { ...process.env },
139
+ });
140
+
141
+ this.process.stdout.on("data", (data) => this._onData(data.toString()));
142
+ this.process.stderr.on("data", (data) => {
143
+ const msg = data.toString().trim();
144
+ if (msg && !msg.startsWith("npm WARN")) console.error(`[${this.name}] ${msg}`);
145
+ });
146
+ this.process.on("error", (err) => {
147
+ console.error(`[${this.name}] spawn error:`, err.message);
148
+ this.ready = false;
149
+ resolve(false);
150
+ });
151
+ this.process.on("exit", (code) => {
152
+ console.log(`[${this.name}] exited with code ${code}`);
153
+ this.ready = false;
154
+ });
155
+
156
+ this._send("initialize", {
157
+ protocolVersion: "2024-11-05",
158
+ capabilities: {},
159
+ clientInfo: { name: "mcp-bridge", version: "2.0.0" },
160
+ }).then((result) => {
161
+ if (result && !result.error) {
162
+ this._notify("notifications/initialized", {});
163
+ return this._send("tools/list", {});
164
+ }
165
+ return null;
166
+ }).then((result) => {
167
+ if (result && result.tools) {
168
+ this.tools = result.tools.map(t => ({
169
+ ...t,
170
+ _originalName: t.name,
171
+ _backend: this.name,
172
+ }));
173
+ this.ready = true;
174
+ console.log(`[${this.name}] ${this.tools.length} tools loaded`);
175
+ }
176
+ resolve(this.ready);
177
+ }).catch((err) => {
178
+ console.error(`[${this.name}] init failed:`, err.message);
179
+ resolve(false);
180
+ });
181
+
182
+ setTimeout(() => { if (!this.ready) resolve(false); }, 60000);
183
+ } catch (err) {
184
+ console.error(`[${this.name}] failed to start:`, err.message);
185
+ resolve(false);
186
+ }
187
+ });
188
+ }
189
+
190
+ _onData(chunk) {
191
+ this.buffer += chunk;
192
+ const lines = this.buffer.split("\n");
193
+ this.buffer = lines.pop() || "";
194
+ for (const line of lines) {
195
+ const trimmed = line.trim();
196
+ if (!trimmed) continue;
197
+ try {
198
+ const msg = JSON.parse(trimmed);
199
+ if (msg.id && this.pending.has(msg.id)) {
200
+ const { resolve } = this.pending.get(msg.id);
201
+ this.pending.delete(msg.id);
202
+ resolve(msg.result || msg.error || {});
203
+ }
204
+ } catch { /* skip non-JSON */ }
205
+ }
206
+ }
207
+
208
+ _send(method, params) {
209
+ return new Promise((resolve, reject) => {
210
+ if (!this.process || this.process.killed) {
211
+ return reject(new Error(`${this.name} process not running`));
212
+ }
213
+ const id = randomUUID();
214
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
215
+ this.pending.set(id, { resolve, reject });
216
+ this.process.stdin.write(msg);
217
+ setTimeout(() => {
218
+ if (this.pending.has(id)) {
219
+ this.pending.delete(id);
220
+ reject(new Error(`${this.name} timeout for ${method}`));
221
+ }
222
+ }, 30000);
223
+ });
224
+ }
225
+
226
+ _notify(method, params) {
227
+ if (!this.process || this.process.killed) return;
228
+ this.process.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
229
+ }
230
+
231
+ async callTool(originalName, args) {
232
+ if (!this.ready) return { error: `${this.name} backend not available` };
233
+ try {
234
+ return await this._send("tools/call", { name: originalName, arguments: args });
235
+ } catch (err) {
236
+ return { error: err.message };
237
+ }
238
+ }
239
+
240
+ stop() {
241
+ if (this.process && !this.process.killed) {
242
+ this.process.kill("SIGTERM");
243
+ this.process = null;
244
+ }
245
+ this.ready = false;
246
+ this.tools = [];
247
+ }
248
+ }
249
+
250
+ // =============================================================================
251
+ // BACKEND REGISTRY
252
+ // =============================================================================
253
+
254
+ const BACKEND_DEFS = [
255
+ { name: "ruvector", command: "npx", args: ["-y", "ruvector", "mcp", "start"], groups: ["intelligence"] },
256
+ { name: "ruflo", command: "npx", args: ["-y", "ruflo", "mcp", "start"], groups: ["agents", "memory", "devtools", "security", "browser", "neural"] },
257
+ { name: "agentic-flow", command: "npx", args: ["-y", "agentic-flow@alpha", "mcp", "start"], groups: ["agentic-flow"] },
258
+ { name: "claude", command: "claude", args: ["mcp", "serve"], groups: ["claude-code"] },
259
+ { name: "gemini-mcp", command: "npx", args: ["-y", "gemini-mcp-server"], groups: ["gemini"] },
260
+ { name: "codex", command: "npx", args: ["-y", "@openai/codex", "mcp-server"], groups: ["codex"] },
261
+ ];
262
+
263
+ const mcpBackends = new Map();
264
+ let allBackendTools = []; // all tools from all backends (pre-filter)
265
+
266
+ function isBackendNeeded(backendDef) {
267
+ return backendDef.groups.some(g => TOOL_GROUPS[g]?.enabled);
268
+ }
269
+
270
+ // Filter tools from a backend based on which groups are enabled
271
+ function filterToolsByGroups(tools, backendName) {
272
+ const enabledGroups = Object.entries(TOOL_GROUPS)
273
+ .filter(([, g]) => g.enabled && g.source === backendName);
274
+
275
+ if (enabledGroups.length === 0) return [];
276
+
277
+ // If any enabled group has no prefixes defined, include all tools from that backend
278
+ const hasWildcard = enabledGroups.some(([, g]) => !g.prefixes);
279
+ if (hasWildcard) return tools;
280
+
281
+ const enabledPrefixes = enabledGroups.flatMap(([, g]) => g.prefixes || []);
282
+ return tools.filter(t => enabledPrefixes.some(p => t._originalName.startsWith(p)));
283
+ }
284
+
285
+ // Get the final filtered tool list with namespaced names
286
+ function getActiveTools() {
287
+ const filtered = [];
288
+ for (const [backendName, client] of mcpBackends) {
289
+ const accepted = filterToolsByGroups(client.tools, backendName);
290
+ for (const t of accepted) {
291
+ filtered.push({ ...t, name: `${backendName}__${t._originalName}` });
292
+ }
293
+ }
294
+ return filtered;
295
+ }
296
+
297
+ async function initBackends() {
298
+ const needed = BACKEND_DEFS.filter(isBackendNeeded);
299
+ if (needed.length === 0) return;
300
+
301
+ console.log(`Starting ${needed.length} MCP backends: ${needed.map(b => b.name).join(", ")}`);
302
+
303
+ await Promise.allSettled(
304
+ needed.map(async (b) => {
305
+ const client = new StdioMcpClient(b.name, b.command, b.args);
306
+ const ok = await client.start();
307
+ if (ok) {
308
+ mcpBackends.set(b.name, client);
309
+ } else {
310
+ console.warn(`[${b.name}] failed to start`);
311
+ }
312
+ })
313
+ );
314
+
315
+ allBackendTools = getActiveTools();
316
+ console.log(`MCP backends: ${mcpBackends.size} active, ${allBackendTools.length} tools (filtered by groups)`);
317
+ }
318
+
319
+ process.on("SIGTERM", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
320
+ process.on("SIGINT", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
321
+
322
+ // =============================================================================
323
+ // BUILT-IN TOOLS (core group — always on)
324
+ // =============================================================================
325
+
326
+ const BUILTIN_TOOLS = [
327
+ {
328
+ name: "search",
329
+ description: "Search your knowledge base for relevant information.",
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {
333
+ query: { type: "string", description: "Natural language search query" },
334
+ limit: { type: "number", description: "Max results (default 5)", default: 5 },
335
+ },
336
+ required: ["query"],
337
+ },
338
+ },
339
+ {
340
+ name: "web_research",
341
+ description: "Search the web, fact-check claims, compare items, or conduct deep research. Actions: 'search' (quick), 'research' (deep report), 'compare' (side-by-side), 'fact_check' (verify claims), 'goap' (comprehensive multi-step research with verification).",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ action: { type: "string", enum: ["search", "research", "compare", "fact_check", "goap"], description: "Research action type", default: "search" },
346
+ query: { type: "string", description: "Search query or topic" },
347
+ items: { type: "array", items: { type: "string" }, description: "Items to compare (for 'compare')" },
348
+ claim: { type: "string", description: "Claim to verify (for 'fact_check')" },
349
+ verify: { type: "boolean", description: "Verify results in goap mode", default: true },
350
+ },
351
+ required: ["query"],
352
+ },
353
+ },
354
+ {
355
+ name: "guidance",
356
+ description: "Get instructions on how to use the available tool groups and services. Call this FIRST when unsure which tool to use, when a user asks 'what can you do?', or when you need to understand a specific tool group. Returns structured guidance for the AI on tool selection and usage patterns.",
357
+ inputSchema: {
358
+ type: "object",
359
+ properties: {
360
+ topic: {
361
+ type: "string",
362
+ enum: ["overview", "groups", "intelligence", "agents", "memory", "devtools", "security", "browser", "neural", "agentic-flow", "claude-code", "gemini", "codex", "tool"],
363
+ description: "What to get guidance on. Use 'overview' for capabilities summary, 'groups' to see all tool groups and their status, or a specific group name for detailed usage instructions.",
364
+ default: "overview",
365
+ },
366
+ tool_name: { type: "string", description: "Specific tool name to get detailed usage info (when topic='tool')" },
367
+ },
368
+ },
369
+ },
370
+ ];
371
+
372
+ // =============================================================================
373
+ // GUIDANCE ENGINE — AI-facing instruction system
374
+ // =============================================================================
375
+
376
+ function getGuidance(topic, toolName) {
377
+ const activeGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled);
378
+ const inactiveGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => !g.enabled);
379
+ const externalTools = getActiveTools();
380
+
381
+ if (topic === "overview") {
382
+ return {
383
+ guidance: `# Tool Capabilities Overview
384
+
385
+ You have access to ${BUILTIN_TOOLS.length + externalTools.length} tools organized into ${activeGroups.length} active groups.
386
+
387
+ ## Active Groups
388
+ ${activeGroups.map(([name, g]) => {
389
+ const count = name === "core" ? BUILTIN_TOOLS.length : externalTools.filter(t => t.name.startsWith(g.source + "__")).length;
390
+ return `- **${name}** (${count} tools) — ${g.description}`;
391
+ }).join("\n")}
392
+
393
+ ## Inactive Groups (can be enabled)
394
+ ${inactiveGroups.map(([name, g]) => `- **${name}** — ${g.description}`).join("\n") || "None"}
395
+
396
+ ## Quick Decision Guide
397
+ - **Knowledge questions** → use \`search\` first, then \`web_research\` if needed
398
+ - **Current events / facts** → use \`web_research\` with action 'search' or 'goap'
399
+ - **Complex research** → use \`web_research\` with action 'goap' (multi-step pipeline)
400
+ - **"What can you do?"** → call \`guidance\` with topic 'groups'
401
+ - **Memory / recall** → use tools from the \`memory\` group
402
+ - **Agent orchestration** → use tools from the \`agents\` group
403
+ - **Code analysis / performance** → use tools from the \`devtools\` group
404
+
405
+ ## Rules
406
+ 1. Call tools FIRST, then present results conversationally
407
+ 2. Never show raw JSON — synthesize results naturally
408
+ 3. For complex questions, prefer GOAP pipeline (web_research action='goap')
409
+ 4. Call \`guidance\` with a specific group name to learn how to use that group's tools`,
410
+ topic: "overview",
411
+ };
412
+ }
413
+
414
+ if (topic === "groups") {
415
+ const groupList = Object.entries(TOOL_GROUPS).map(([name, g]) => {
416
+ const status = g.enabled ? "ACTIVE" : "INACTIVE";
417
+ const toolCount = name === "core" ? BUILTIN_TOOLS.length :
418
+ externalTools.filter(t => {
419
+ const backend = t._backend;
420
+ return g.source === backend && (!g.prefixes || g.prefixes.some(p => t._originalName.startsWith(p)));
421
+ }).length;
422
+ return `| ${name} | ${status} | ${toolCount} | ${g.description} |`;
423
+ });
424
+
425
+ return {
426
+ guidance: `# Tool Groups\n\n| Group | Status | Tools | Description |\n|-------|--------|-------|-------------|\n${groupList.join("\n")}`,
427
+ topic: "groups",
428
+ };
429
+ }
430
+
431
+ // Specific group guidance
432
+ const groupGuides = {
433
+ intelligence: `# Intelligence Group (ruvector)
434
+
435
+ Self-learning intelligence tools for routing and vector memory.
436
+
437
+ ## Key Tools
438
+ - **ruvector__hooks_route** — Route a task to the best agent type. Call with a task description.
439
+ - **ruvector__hooks_remember** — Store context/knowledge in vector memory for later recall.
440
+ - **ruvector__hooks_recall** — Search vector memory semantically. Good for finding past context.
441
+ - **ruvector__hooks_pretrain** — Bootstrap intelligence from a code repository.
442
+ - **ruvector__hooks_build_agents** — Generate optimized agent configurations.
443
+ - **ruvector__hooks_stats** — Get intelligence statistics and learning metrics.
444
+
445
+ ## When to Use
446
+ - Before starting complex tasks: route to find the best agent approach
447
+ - To store important findings for cross-session memory
448
+ - To recall previously stored patterns or solutions`,
449
+
450
+ agents: `# Agents & Orchestration Group (ruflo)
451
+
452
+ Multi-agent lifecycle management, swarm coordination, and task workflows.
453
+
454
+ ## Key Tools
455
+ - **ruflo__agent_spawn** — Create a new agent with specific capabilities
456
+ - **ruflo__agent_list** — List all active agents
457
+ - **ruflo__swarm_init** — Initialize a swarm with a topology (mesh, hierarchical, ring, star)
458
+ - **ruflo__task_create** — Create and assign tasks
459
+ - **ruflo__workflow_create** — Define multi-step workflows
460
+ - **ruflo__workflow_execute** — Execute a workflow
461
+ - **ruflo__hive-mind_init** — Start collective intelligence coordination
462
+ - **ruflo__coordination_orchestrate** — Multi-agent coordination
463
+
464
+ ## When to Use
465
+ - Complex tasks requiring multiple agents working together
466
+ - Pipeline workflows with sequential or parallel steps
467
+ - Distributed task management`,
468
+
469
+ memory: `# Memory & Knowledge Group (ruflo)
470
+
471
+ Vector storage, semantic search, AgentDB pattern learning, and embeddings.
472
+
473
+ ## Key Tools
474
+ - **ruflo__memory_store** — Store a value with vector embedding for semantic search
475
+ - **ruflo__memory_search** — Semantic search across stored memories (HNSW-indexed)
476
+ - **ruflo__memory_list** — List stored memory entries
477
+ - **ruflo__agentdb_pattern-store** — Store a reasoning pattern for learning
478
+ - **ruflo__agentdb_pattern-search** — Search for similar reasoning patterns
479
+ - **ruflo__agentdb_context-synthesize** — Synthesize context from stored memories
480
+ - **ruflo__embeddings_generate** — Generate vector embeddings for text
481
+ - **ruflo__embeddings_search** — Semantic similarity search
482
+
483
+ ## When to Use
484
+ - Persistent knowledge storage across sessions
485
+ - Finding similar past solutions or patterns
486
+ - Building semantic search over custom data`,
487
+
488
+ devtools: `# Dev Tools Group (ruflo)
489
+
490
+ Code analysis, performance profiling, GitHub integration, and terminal access.
491
+
492
+ ## Key Tools
493
+ - **ruflo__analyze_diff** — Analyze git diff for risk and change classification
494
+ - **ruflo__performance_benchmark** — Run performance benchmarks
495
+ - **ruflo__performance_bottleneck** — Detect performance bottlenecks
496
+ - **ruflo__github_repo_analyze** — Analyze a GitHub repository
497
+ - **ruflo__github_pr_manage** — Manage pull requests
498
+ - **ruflo__terminal_execute** — Execute commands in a terminal session
499
+
500
+ ## When to Use
501
+ - Code review and change risk assessment
502
+ - Performance analysis and optimization
503
+ - GitHub repository management`,
504
+
505
+ security: `# Security & Safety Group (ruflo)
506
+
507
+ AI defence, PII detection, and claims-based authorization.
508
+
509
+ ## Key Tools
510
+ - **ruflo__aidefence_scan** — Scan text for AI manipulation attempts
511
+ - **ruflo__aidefence_has_pii** — Check for PII (emails, phones, SSNs)
512
+ - **ruflo__aidefence_is_safe** — Quick safety check on input
513
+ - **ruflo__claims_claim** — Claim an issue for work
514
+ - **ruflo__claims_board** — Visual board of all claims
515
+
516
+ ## When to Use
517
+ - Input validation and safety checking
518
+ - PII detection before processing sensitive data
519
+ - Work item management across agents`,
520
+
521
+ browser: `# Browser Automation Group (ruflo)
522
+
523
+ Headless browser control for web interaction and testing.
524
+
525
+ ## Key Tools
526
+ - **ruflo__browser_open** — Navigate to a URL
527
+ - **ruflo__browser_click** — Click elements by reference
528
+ - **ruflo__browser_fill** — Fill form inputs
529
+ - **ruflo__browser_screenshot** — Capture page screenshots
530
+ - **ruflo__browser_snapshot** — Get accessibility tree for AI parsing
531
+ - **ruflo__browser_eval** — Execute JavaScript in page context
532
+
533
+ ## When to Use
534
+ - Web scraping and data extraction
535
+ - Automated testing (E2E)
536
+ - Form filling and web interaction`,
537
+
538
+ neural: `# Neural & DAA Group (ruflo)
539
+
540
+ Neural network operations and Decentralized Autonomous Agents.
541
+
542
+ ## Key Tools
543
+ - **ruflo__neural_train** — Train a neural model
544
+ - **ruflo__neural_predict** — Make predictions
545
+ - **ruflo__daa_agent_create** — Create an autonomous agent
546
+ - **ruflo__daa_workflow_create** — Create autonomous workflows
547
+ - **ruflo__daa_knowledge_share** — Share knowledge between agents
548
+
549
+ ## When to Use
550
+ - Pattern learning and prediction
551
+ - Autonomous agent workflows
552
+ - Knowledge transfer between agents`,
553
+
554
+ "agentic-flow": `# Agentic Flow Group (agentic-flow@alpha)
555
+
556
+ Execute 66+ specialized agents with boosted code editing and AgentDB.
557
+
558
+ ## Key Tools
559
+ - **agentic-flow__agentic_flow_agent** — Execute any of 66+ specialized agents
560
+ - **agentic-flow__agentic_flow_list_agents** — List available agent types
561
+ - **agentic-flow__agent_booster_edit_file** — 352x faster code editing
562
+ - **agentic-flow__agent_booster_batch_edit** — Multi-file refactoring
563
+ - **agentic-flow__agentdb_pattern_store** — Store reasoning patterns
564
+ - **agentic-flow__agentdb_pattern_search** — Search similar patterns
565
+
566
+ ## When to Use
567
+ - Complex code generation with specialized agents
568
+ - Batch code refactoring across files
569
+ - Agent selection when you need the right specialist`,
570
+
571
+ "claude-code": `# Claude Code Group
572
+
573
+ Anthropic Claude Code MCP server — full coding agent capabilities.
574
+
575
+ Requires: ANTHROPIC_API_KEY environment variable.
576
+
577
+ ## Capabilities
578
+ - File reading and editing
579
+ - Bash command execution
580
+ - Code analysis and generation
581
+ - Project exploration
582
+
583
+ ## When to Use
584
+ - When you need a second AI perspective on code
585
+ - Complex refactoring tasks
586
+ - Code review and analysis`,
587
+
588
+ gemini: `# Gemini MCP Group
589
+
590
+ Google Gemini with conversation context management.
591
+
592
+ Requires: GOOGLE_API_KEY environment variable (already set for Gemini models).
593
+
594
+ ## Capabilities
595
+ - Conversation context management
596
+ - Multimodal processing
597
+ - Google Search grounding
598
+
599
+ ## When to Use
600
+ - Extended context conversations
601
+ - Multimodal content processing`,
602
+
603
+ codex: `# Codex Group
604
+
605
+ OpenAI Codex coding agent.
606
+
607
+ Requires: OPENAI_API_KEY environment variable (already set for OpenAI models).
608
+
609
+ ## Capabilities
610
+ - Code generation and execution
611
+ - Code completion
612
+ - Code explanation
613
+
614
+ ## When to Use
615
+ - Code generation tasks
616
+ - Quick code completions
617
+ - Code explanation and documentation`,
618
+ };
619
+
620
+ if (topic === "tool" && toolName) {
621
+ const allTools = [...BUILTIN_TOOLS, ...externalTools];
622
+ const tool = allTools.find(t => t.name === toolName);
623
+ if (tool) {
624
+ const props = Object.entries(tool.inputSchema?.properties || {})
625
+ .map(([k, v]) => `- **${k}** (${v.type}) — ${v.description || ""}`)
626
+ .join("\n");
627
+ return { guidance: `# ${tool.name}\n\n${tool.description}\n\n## Parameters\n${props}`, topic: "tool" };
628
+ }
629
+ return { guidance: `Tool '${toolName}' not found. Call guidance with topic='groups' to see available tools.`, topic: "tool" };
630
+ }
631
+
632
+ if (groupGuides[topic]) {
633
+ const group = TOOL_GROUPS[topic];
634
+ if (!group?.enabled) {
635
+ return { guidance: `# ${topic} — INACTIVE\n\n${group?.description || ""}\n\nThis group is not enabled. Set the appropriate MCP_GROUP_* env var to "true" to activate it.`, topic };
636
+ }
637
+ return { guidance: groupGuides[topic], topic };
638
+ }
639
+
640
+ return { guidance: `Unknown topic '${topic}'. Use 'overview', 'groups', or a specific group name.`, topic };
641
+ }
642
+
643
+ // =============================================================================
644
+ // SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
645
+ // =============================================================================
646
+
647
+ const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;
648
+
649
+ function assertSafeUrl(rawUrl) {
650
+ let parsed;
651
+ try {
652
+ parsed = new URL(rawUrl);
653
+ } catch {
654
+ throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
655
+ }
656
+ if (parsed.protocol !== "https:") {
657
+ throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
658
+ }
659
+ const host = parsed.hostname;
660
+ if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
661
+ throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
662
+ }
663
+ }
664
+
665
+ // =============================================================================
666
+ // HELPER — Call a backend Cloud Function / API
667
+ // =============================================================================
668
+
669
+ async function callCloudFunction(url, payload, timeoutMs = 25000) {
670
+ // Validate the URL before making any network request.
671
+ assertSafeUrl(url);
672
+ const controller = new AbortController();
673
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
674
+ try {
675
+ const resp = await fetch(url, {
676
+ method: "POST",
677
+ headers: { "Content-Type": "application/json" },
678
+ body: JSON.stringify(payload),
679
+ signal: controller.signal,
680
+ });
681
+ return await resp.json();
682
+ } catch (err) {
683
+ if (err.name === "AbortError") return { error: "Request timed out", timeout: timeoutMs };
684
+ return { error: err.message };
685
+ } finally {
686
+ clearTimeout(timer);
687
+ }
688
+ }
689
+
690
+ // =============================================================================
691
+ // GOAP SEARCH PIPELINE
692
+ // =============================================================================
693
+
694
+ async function executeGoapSearch(query, args) {
695
+ const researchUrl = CLOUD_FUNCTIONS.research;
696
+ if (!researchUrl) return { error: "GOAP requires a 'research' URL in CLOUD_FUNCTIONS" };
697
+
698
+ const startTime = Date.now();
699
+
700
+ const composeResult = await callCloudFunction(researchUrl, {
701
+ action: "search",
702
+ query: `Break this question into 3-4 distinct search queries that would help answer it comprehensively. Return ONLY the queries, one per line:\n\n${query}`,
703
+ }, 30000);
704
+
705
+ let searchQueries = [query];
706
+ if (composeResult && !composeResult.error) {
707
+ const answer = composeResult.result?.answer || composeResult.answer || "";
708
+ const lines = answer.split("\n").map(l => l.replace(/^[\d\-\*\.\)]+\s*/, "").trim()).filter(l => l.length > 5 && l.length < 200);
709
+ if (lines.length >= 2) searchQueries = lines.slice(0, 4);
710
+ }
711
+
712
+ const searchResults = await Promise.all(
713
+ searchQueries.map(q => callCloudFunction(researchUrl, { action: "search", query: q }, 30000))
714
+ );
715
+
716
+ const allSources = [], allAnswers = [];
717
+ for (let i = 0; i < searchResults.length; i++) {
718
+ const r = searchResults[i];
719
+ if (r && !r.error && r.success !== false) {
720
+ const answer = r.result?.answer || r.answer || "";
721
+ if (answer) allAnswers.push({ query: searchQueries[i], answer });
722
+ const gm = r.result?.groundingMetadata || r.groundingMetadata || {};
723
+ if (gm.sources) allSources.push(...gm.sources);
724
+ }
725
+ }
726
+
727
+ const uniqueSources = [];
728
+ const seenUrls = new Set();
729
+ for (const src of allSources) {
730
+ const url = src.url || src.uri || "";
731
+ if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
732
+ }
733
+
734
+ const synthesisInput = allAnswers.map(a => `## ${a.query}\n${a.answer}`).join("\n\n");
735
+ const synthesisResult = await callCloudFunction(researchUrl, {
736
+ action: "research",
737
+ topic: `Synthesize these findings into a comprehensive answer to: "${query}"\n\nFindings:\n${synthesisInput}`,
738
+ }, 60000);
739
+
740
+ const synthesizedAnswer = synthesisResult?.result?.answer || synthesisResult?.answer || synthesisInput;
741
+ const synthGm = synthesisResult?.result?.groundingMetadata || {};
742
+ if (synthGm.sources) {
743
+ for (const src of synthGm.sources) {
744
+ const url = src.url || src.uri || "";
745
+ if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
746
+ }
747
+ }
748
+
749
+ let verification = { verified: true, confidence: "high" };
750
+ if (args.verify !== false && synthesizedAnswer.length > 100) {
751
+ const vr = await callCloudFunction(researchUrl, {
752
+ action: "fact_check", claim: synthesizedAnswer.substring(0, 500),
753
+ }, 30000);
754
+ if (vr && !vr.error && vr.result) {
755
+ verification = {
756
+ verified: vr.result.verdict !== "FALSE",
757
+ verdict: vr.result.verdict,
758
+ confidence: vr.result.confidence || "medium",
759
+ details: vr.result.analysis,
760
+ };
761
+ }
762
+ }
763
+
764
+ return {
765
+ answer: synthesizedAnswer, pipeline: "goap",
766
+ steps: { queries_composed: searchQueries.length, searches_executed: searchResults.filter(r => !r?.error).length, sources_found: uniqueSources.length, verification },
767
+ sources: uniqueSources.slice(0, 10), searchQueries, duration_ms: Date.now() - startTime,
768
+ };
769
+ }
770
+
771
+ // =============================================================================
772
+ // TOOL EXECUTOR
773
+ // =============================================================================
774
+
775
+ // ADR-166 §6 Phase 1d + 2a — server-side tool gate.
776
+ // Enforced HERE (not just in the autopilot handler) so /mcp, /mcp/:group,
777
+ // autopilot, and any future path share ONE denial gate. Was the missing
778
+ // link that made the disclosed unauthenticated-RCE chain reach shell.
779
+ const DANGEROUS_TOOLS = Object.freeze(new Set([
780
+ "terminal_execute",
781
+ "ruflo__terminal_execute",
782
+ "devtools__terminal_execute",
783
+ ]));
784
+ function isTerminalTool(name) {
785
+ return DANGEROUS_TOOLS.has(name) || /terminal_execute/i.test(name);
786
+ }
787
+ const MCP_ENABLE_TERMINAL = process.env.MCP_ENABLE_TERMINAL === "true";
788
+
789
+ async function executeTool(name, args) {
790
+ // Deny dangerous tools unless the operator explicitly opted in.
791
+ // Enforced on every path (not just autopilot) — root cause of ADR-166 V2/V3.
792
+ if (isTerminalTool(name) && !MCP_ENABLE_TERMINAL) {
793
+ return {
794
+ error:
795
+ `Tool "${name}" is disabled by default. Set MCP_ENABLE_TERMINAL=true to allow.`,
796
+ code: "TOOL_DISABLED",
797
+ };
798
+ }
799
+ // Validate that search-like tools have a non-empty query to prevent 400 errors
800
+ if (!args || typeof args !== "object") args = {};
801
+ const rawQuery = args.query ?? args.q ?? args.input ?? "";
802
+ const queryStr = typeof rawQuery === "string" ? rawQuery.trim() : String(rawQuery || "").trim();
803
+ const isSearchTool = name === "search" || name === "web_research" || /^(web_)?search/i.test(name);
804
+ if (isSearchTool && !queryStr) {
805
+ return { content: [{ type: "text", text: `No search query provided. Please specify a search query for '${name}'.` }] };
806
+ }
807
+
808
+ switch (name) {
809
+ case "search":
810
+ if (!CLOUD_FUNCTIONS.search) return { error: "search endpoint not configured" };
811
+ return callCloudFunction(CLOUD_FUNCTIONS.search, { query: args.query, limit: args.limit || 5 });
812
+
813
+ case "web_research": {
814
+ const action = args.action || "search";
815
+ if (action === "goap") return executeGoapSearch(args.query, args);
816
+ if (!CLOUD_FUNCTIONS.research) return { error: "research endpoint not configured" };
817
+ const payload = { action };
818
+ if (action === "search") payload.query = args.query;
819
+ else if (action === "research") payload.topic = args.query;
820
+ else if (action === "compare") payload.items = args.items;
821
+ else if (action === "fact_check") payload.claim = args.claim || args.query;
822
+ return callCloudFunction(CLOUD_FUNCTIONS.research, payload, 60000);
823
+ }
824
+
825
+ case "guidance":
826
+ return getGuidance(args.topic || "overview", args.tool_name);
827
+
828
+ default: {
829
+ // Route to external MCP backend
830
+ const activeTools = getActiveTools();
831
+ const extTool = activeTools.find(t => t.name === name);
832
+ if (extTool) {
833
+ const backend = mcpBackends.get(extTool._backend);
834
+ if (backend) return backend.callTool(extTool._originalName, args);
835
+ return { error: `Backend ${extTool._backend} not available` };
836
+ }
837
+ return { error: `Unknown tool: ${name}. Call 'guidance' with topic='groups' to see available tools.` };
838
+ }
839
+ }
840
+ }
841
+
842
+ // =============================================================================
843
+ // PER-GROUP TOOL HELPERS
844
+ // =============================================================================
845
+
846
+ // Get tools for a specific group only
847
+ function getToolsForGroup(groupName) {
848
+ const group = TOOL_GROUPS[groupName];
849
+ if (!group || !group.enabled) return [];
850
+ if (groupName === "core") return BUILTIN_TOOLS;
851
+
852
+ const allActive = getActiveTools();
853
+ if (!group.prefixes) {
854
+ // No prefix filter — return all tools from this backend
855
+ return allActive.filter(t => t._backend === group.source);
856
+ }
857
+ return allActive.filter(t =>
858
+ t._backend === group.source && group.prefixes.some(p => t._originalName.startsWith(p))
859
+ );
860
+ }
861
+
862
+ // Group display names for the Chat UI
863
+ const GROUP_DISPLAY_NAMES = {
864
+ core: "Core Tools",
865
+ intelligence: "Intelligence & Learning",
866
+ agents: "Agents & Orchestration",
867
+ memory: "Memory & Knowledge",
868
+ devtools: "Dev Tools & Analysis",
869
+ security: "Security & Safety",
870
+ browser: "Browser Automation",
871
+ neural: "Neural & DAA",
872
+ "agentic-flow": "Agentic Flow",
873
+ "claude-code": "Claude Code",
874
+ gemini: "Gemini",
875
+ codex: "Codex",
876
+ };
877
+
878
+ // =============================================================================
879
+ // MCP SERVER — Multiple endpoints per group
880
+ // =============================================================================
881
+
882
+ const app = express();
883
+ app.use(express.json({ limit: "10mb" }));
884
+
885
+ // ---------- MCP Streamable HTTP session (#2425 djimit) ----------
886
+ // Streamable-HTTP clients (Codex/RMCP) send `DELETE /mcp` with an
887
+ // `Mcp-Session-Id` header at shutdown. We echo a stable session id back
888
+ // on every /mcp* response so those clients can attach it to the DELETE
889
+ // and to `notifications/initialized` handshakes.
890
+ const MCP_SESSION_ID = randomUUID();
891
+ app.use((req, res, next) => {
892
+ if (req.path.startsWith("/mcp")) {
893
+ res.setHeader("Mcp-Session-Id", MCP_SESSION_ID);
894
+ }
895
+ next();
896
+ });
897
+
898
+ // ---------- CORS middleware (ADR-166 §6 Phase 3b) ----------
899
+ const CORS_ALLOWLIST = (process.env.MCP_CORS_ORIGIN || "*")
900
+ .split(",").map(s => s.trim()).filter(Boolean);
901
+ const CORS_WILDCARD = CORS_ALLOWLIST.length === 1 && CORS_ALLOWLIST[0] === "*";
902
+ app.use((req, res, next) => {
903
+ const origin = req.get("origin") || "";
904
+ if (CORS_WILDCARD) {
905
+ res.setHeader("Access-Control-Allow-Origin", "*");
906
+ } else if (origin && CORS_ALLOWLIST.includes(origin)) {
907
+ res.setHeader("Access-Control-Allow-Origin", origin);
908
+ res.setHeader("Vary", "Origin");
909
+ }
910
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
911
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
912
+ if (req.method === "OPTIONS") return res.sendStatus(204);
913
+ next();
914
+ });
915
+
916
+ // ---------- Auth middleware ----------
917
+ // No-op in local-only mode (MCP_AUTH_TOKEN unset). Enforces 401 when token is set.
918
+ const MCP_TOKEN = process.env.MCP_AUTH_TOKEN || "";
919
+ function requireAuth(req, res, next) {
920
+ if (req.path === "/health") return next();
921
+ if (!MCP_TOKEN) return next();
922
+ const expected = `Bearer ${MCP_TOKEN}`;
923
+ const got = req.get("authorization") || "";
924
+ const ok = got.length === expected.length &&
925
+ timingSafeEqual(Buffer.from(got), Buffer.from(expected));
926
+ if (!ok) return res.status(401).json({ error: "unauthorized" });
927
+ next();
928
+ }
929
+ app.use(requireAuth);
930
+
931
+ // ---------- Shared MCP handler ----------
932
+ function createMcpHandler(groupName) {
933
+ return async (req, res) => {
934
+ const { method, id, params } = req.body;
935
+ try {
936
+ switch (method) {
937
+ case "initialize":
938
+ return res.json({
939
+ jsonrpc: "2.0", id,
940
+ result: {
941
+ protocolVersion: "2024-11-05",
942
+ capabilities: { tools: {} },
943
+ serverInfo: { name: `mcp-bridge/${groupName}`, version: "2.0.0" },
944
+ },
945
+ });
946
+ case "tools/list": {
947
+ const tools = getToolsForGroup(groupName);
948
+ return res.json({ jsonrpc: "2.0", id, result: { tools } });
949
+ }
950
+ case "tools/call": {
951
+ const { name, arguments: toolArgs } = params;
952
+ const result = await executeTool(name, toolArgs || {});
953
+ // If executeTool already returned MCP-formatted content, pass through directly
954
+ const mcpResult = result && Array.isArray(result.content)
955
+ ? { content: result.content }
956
+ : { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
957
+ return res.json({ jsonrpc: "2.0", id, result: mcpResult });
958
+ }
959
+ case "notifications/initialized":
960
+ // MCP streamable-HTTP spec: notifications must return 202 Accepted
961
+ // with an empty body (no jsonrpc envelope).
962
+ return res.status(202).end();
963
+ default:
964
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
965
+ }
966
+ } catch (err) {
967
+ console.error(`MCP error [${groupName}/${method}]:`, err);
968
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
969
+ }
970
+ };
971
+ }
972
+
973
+ function createMcpSseHandler(groupName) {
974
+ return (req, res) => {
975
+ res.setHeader("Content-Type", "text/event-stream");
976
+ res.setHeader("Cache-Control", "no-cache");
977
+ res.setHeader("Connection", "keep-alive");
978
+ res.write(`data: ${JSON.stringify({ type: "endpoint", url: `/mcp/${groupName}` })}\n\n`);
979
+ };
980
+ }
981
+
982
+ // ---------- Register per-group endpoints ----------
983
+ for (const groupName of Object.keys(TOOL_GROUPS)) {
984
+ app.post(`/mcp/${groupName}`, createMcpHandler(groupName));
985
+ app.get(`/mcp/${groupName}`, createMcpSseHandler(groupName));
986
+ // #2425 djimit — streamable-HTTP session cleanup
987
+ app.delete(`/mcp/${groupName}`, (_, res) => res.sendStatus(204));
988
+ }
989
+
990
+ // ---------- Catch-all /mcp — serves ALL enabled tools (backwards-compatible) ----------
991
+ app.post("/mcp", async (req, res) => {
992
+ const { method, id, params } = req.body;
993
+ try {
994
+ switch (method) {
995
+ case "initialize":
996
+ return res.json({
997
+ jsonrpc: "2.0", id,
998
+ result: {
999
+ protocolVersion: "2024-11-05",
1000
+ capabilities: { tools: {} },
1001
+ serverInfo: { name: "mcp-bridge", version: "2.0.0" },
1002
+ },
1003
+ });
1004
+ case "tools/list": {
1005
+ const activeTools = getActiveTools();
1006
+ return res.json({ jsonrpc: "2.0", id, result: { tools: [...BUILTIN_TOOLS, ...activeTools] } });
1007
+ }
1008
+ case "tools/call": {
1009
+ const { name, arguments: toolArgs } = params;
1010
+ const result = await executeTool(name, toolArgs || {});
1011
+ // If executeTool already returned MCP-formatted content, pass through directly
1012
+ const mcpResult = result && Array.isArray(result.content)
1013
+ ? { content: result.content }
1014
+ : { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
1015
+ return res.json({ jsonrpc: "2.0", id, result: mcpResult });
1016
+ }
1017
+ case "notifications/initialized":
1018
+ return res.status(202).end();
1019
+ default:
1020
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
1021
+ }
1022
+ } catch (err) {
1023
+ console.error(`MCP error [${method}]:`, err);
1024
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
1025
+ }
1026
+ });
1027
+
1028
+ app.get("/mcp", (req, res) => {
1029
+ res.setHeader("Content-Type", "text/event-stream");
1030
+ res.setHeader("Cache-Control", "no-cache");
1031
+ res.setHeader("Connection", "keep-alive");
1032
+ res.write(`data: ${JSON.stringify({ type: "endpoint", url: "/mcp" })}\n\n`);
1033
+ });
1034
+
1035
+ // #2425 djimit — streamable-HTTP session cleanup on the catch-all route.
1036
+ app.delete("/mcp", (_, res) => res.sendStatus(204));
1037
+
1038
+ // ---------- GET /mcp-servers — returns MCP_SERVERS JSON for Chat UI config ----------
1039
+ app.get("/mcp-servers", (_, res) => {
1040
+ const servers = [];
1041
+ for (const [name, group] of Object.entries(TOOL_GROUPS)) {
1042
+ if (!group.enabled) continue;
1043
+ const tools = getToolsForGroup(name);
1044
+ if (tools.length === 0) continue;
1045
+ servers.push({
1046
+ name: GROUP_DISPLAY_NAMES[name] || name,
1047
+ url: `/mcp/${name}`,
1048
+ tools: tools.length,
1049
+ group: name,
1050
+ });
1051
+ }
1052
+ res.json(servers);
1053
+ });
1054
+
1055
+ // =============================================================================
1056
+ // CHAT COMPLETIONS PROXY
1057
+ // =============================================================================
1058
+
1059
+ const PROVIDER_ROUTES = {
1060
+ openai: { baseURL: "https://api.openai.com/v1/chat/completions", getKey: () => process.env.OPENAI_API_KEY },
1061
+ gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", getKey: () => process.env.GOOGLE_API_KEY },
1062
+ openrouter: { baseURL: "https://openrouter.ai/api/v1/chat/completions", getKey: () => process.env.OPENROUTER_API_KEY },
1063
+ };
1064
+
1065
+ function resolveProvider(model) {
1066
+ if (typeof model === "string") {
1067
+ if (model.startsWith("gemini-")) return "gemini";
1068
+ if (model.includes("/")) return "openrouter";
1069
+ }
1070
+ return "openai";
1071
+ }
1072
+
1073
+ // =============================================================================
1074
+ // SYSTEM PROMPT — Injected server-side into every chat completion request
1075
+ // =============================================================================
1076
+ // This comprehensive prompt teaches the AI how to use all 200+ MCP tools
1077
+ // across 5 groups. It is injected as the first system message, ensuring
1078
+ // consistent behavior regardless of what preprompt the Chat UI sends.
1079
+
1080
+ function buildSystemPrompt() {
1081
+ // Build dynamic group status
1082
+ const enabledGroups = Object.entries(TOOL_GROUPS)
1083
+ .filter(([, g]) => g.enabled)
1084
+ .map(([name]) => name);
1085
+
1086
+ return `You are an intelligent AI assistant with powerful tools organized into ${enabledGroups.length} active groups. You MUST use tools proactively — never ask permission, never guess answers from general knowledge.
1087
+
1088
+ IMPORTANT: Call \`guidance\` with topic='overview' if you are ever unsure which tool to use.
1089
+
1090
+ # Tool Groups
1091
+
1092
+ Your tools are organized into groups. Each tool name is prefixed with its backend (e.g., \`ruflo__agent_spawn\`, \`ruvector__hooks_route\`). Always use the full prefixed name when calling tools.
1093
+
1094
+ ## Group 1: Core Tools (always on)
1095
+ Built-in tools available in every conversation.
1096
+
1097
+ - **search** — Search the knowledge base for documents, procedures, how-tos.
1098
+ ALWAYS search before answering knowledge questions — never answer from general knowledge alone.
1099
+ - **web_research** — Web search, deep research, comparisons, fact-checking.
1100
+ Actions: \`search\` (quick), \`research\` (deep report), \`compare\` (side-by-side), \`fact_check\` (verify), \`goap\` (comprehensive multi-step — BEST for important questions)
1101
+ The GOAP pipeline automatically decomposes questions into 3-4 parallel searches, synthesizes findings, and verifies accuracy.
1102
+ - **guidance** — Get help on any tool group, specific tool usage, or capabilities overview.
1103
+ Topics: \`overview\`, \`groups\`, \`agents\`, \`memory\`, \`intelligence\`, \`devtools\`
1104
+ For specific tool help: \`guidance(topic='tool', tool_name='ruflo__agent_spawn')\`
1105
+
1106
+ ## Group 2: Intelligence & Learning (ruvector)
1107
+ Pattern learning, routing, code analysis, and trajectory tracking. ${TOOL_GROUPS.intelligence.enabled ? "ACTIVE" : "DISABLED"}
1108
+
1109
+ ### Essential Intelligence Tools:
1110
+ - **ruvector__hooks_route** — Route a task to the optimal agent type. Call this FIRST for complex tasks.
1111
+ \`{"task": "describe what needs to be done", "context": ["relevant info"]}\`
1112
+ Returns ranked agent recommendations with confidence scores.
1113
+ - **ruvector__hooks_remember** — Store a key-value pair in persistent memory for cross-session recall.
1114
+ \`{"key": "pattern-name", "value": "what to remember", "namespace": "patterns"}\`
1115
+ - **ruvector__hooks_recall** — Retrieve a previously stored memory by key.
1116
+ - **ruvector__hooks_suggest_context** — Get contextual suggestions based on current work.
1117
+ - **ruvector__hooks_swarm_recommend** — Get swarm topology recommendation for a task type.
1118
+ - **ruvector__hooks_capabilities** — List all intelligence system capabilities.
1119
+
1120
+ ### Code Analysis:
1121
+ - **ruvector__hooks_ast_analyze** — Analyze code structure (AST) of a file.
1122
+ - **ruvector__hooks_ast_complexity** — Get complexity metrics for code.
1123
+ - **ruvector__hooks_security_scan** — Scan code for security vulnerabilities.
1124
+ - **ruvector__hooks_diff_analyze** — Analyze a code diff for risk and impact.
1125
+ - **ruvector__hooks_diff_similar** — Find similar past diffs/changes.
1126
+
1127
+ ### Trajectory Learning (for multi-step tasks):
1128
+ - **ruvector__hooks_trajectory_begin** — Start tracking a multi-step task for learning.
1129
+ - **ruvector__hooks_trajectory_step** — Record a step in the current trajectory.
1130
+ - **ruvector__hooks_trajectory_end** — End trajectory, triggering pattern extraction.
1131
+
1132
+ ### Memory & Compression:
1133
+ - **ruvector__hooks_compress** — Compress/summarize long text for efficient storage.
1134
+ - **ruvector__hooks_rag_context** — Get RAG context for a query from stored knowledge.
1135
+ - **ruvector__hooks_learn** — Force the system to learn from provided examples.
1136
+ - **ruvector__hooks_batch_learn** — Learn from multiple examples at once.
1137
+ - **ruvector__hooks_stats** — View learning statistics and metrics.
1138
+ - **ruvector__hooks_doctor** — Run diagnostics on the intelligence system.
1139
+
1140
+ ## Group 3: Agents & Orchestration (ruflo)
1141
+ Spawn agents, coordinate swarms, manage tasks and workflows. ${TOOL_GROUPS.agents.enabled ? "ACTIVE" : "DISABLED"}
1142
+
1143
+ ### Agent Lifecycle:
1144
+ - **ruflo__agent_spawn** — Create a new specialized agent.
1145
+ \`{"type": "coder|researcher|tester|reviewer|architect|security", "name": "optional-name"}\`
1146
+ Agent types and when to use them:
1147
+ - \`coder\` — Write code, implement features, fix bugs
1148
+ - \`researcher\` — Find information, analyze documentation, investigate
1149
+ - \`tester\` — Write tests, run test suites, validate behavior
1150
+ - \`reviewer\` — Review code quality, security, best practices
1151
+ - \`architect\` — Design systems, plan architectures, evaluate trade-offs
1152
+ - \`security\` — Audit security, find vulnerabilities, recommend fixes
1153
+ - **ruflo__agent_status** — Check an agent's current state. \`{"agentId": "agent-xxx"}\`
1154
+ - **ruflo__agent_list** — List all active agents with their states.
1155
+ - **ruflo__agent_terminate** — Stop an agent. \`{"agentId": "agent-xxx"}\`
1156
+ - **ruflo__agent_health** — Health check across all agents.
1157
+ - **ruflo__agent_pool** — View the agent pool and available capacity.
1158
+
1159
+ ### Swarm Coordination:
1160
+ - **ruflo__swarm_init** — Initialize a multi-agent swarm.
1161
+ \`{"topology": "hierarchical|mesh|ring|star", "maxAgents": 8, "strategy": "balanced|specialized|adaptive"}\`
1162
+ - \`hierarchical\` — Coordinator + workers, best for structured tasks (anti-drift)
1163
+ - \`mesh\` — Peer-to-peer, best for collaborative work
1164
+ - \`ring\` — Sequential pipeline, best for ordered processing
1165
+ - \`star\` — Central hub, best for fan-out parallel work
1166
+ - **ruflo__swarm_status** — Get swarm health, topology, and agent states.
1167
+ - **ruflo__swarm_health** — Detailed health metrics for the swarm.
1168
+ - **ruflo__swarm_shutdown** — Tear down a swarm and all its agents.
1169
+
1170
+ ### Task Management:
1171
+ - **ruflo__task_create** — Create a tracked task.
1172
+ \`{"description": "what needs to be done", "priority": "low|normal|high|critical"}\`
1173
+ - **ruflo__task_status** — Check task progress. \`{"taskId": "task-xxx"}\`
1174
+ - **ruflo__task_list** — List all tasks with their statuses.
1175
+ - **ruflo__task_complete** — Mark a task as done. \`{"taskId": "task-xxx"}\`
1176
+ - **ruflo__task_update** — Update task details, status, or assignment.
1177
+ - **ruflo__task_cancel** — Cancel a task.
1178
+
1179
+ ### Workflow Orchestration:
1180
+ - **ruflo__workflow_create** — Define a multi-step workflow with dependencies.
1181
+ - **ruflo__workflow_execute** — Run a workflow. \`{"workflowId": "wf-xxx"}\`
1182
+ - **ruflo__workflow_status** — Check workflow progress.
1183
+ - **ruflo__workflow_template** — Use a pre-built workflow template.
1184
+ - **ruflo__workflow_pause** / **ruflo__workflow_resume** — Control workflow execution.
1185
+
1186
+ ### Hive-Mind (Distributed Consensus):
1187
+ - **ruflo__hive-mind_init** — Start distributed consensus system.
1188
+ - **ruflo__hive-mind_spawn** — Add an agent to the hive.
1189
+ - **ruflo__hive-mind_consensus** — Run consensus vote across agents.
1190
+ - **ruflo__hive-mind_broadcast** — Send message to all hive agents.
1191
+ - **ruflo__hive-mind_memory** — Access shared hive memory.
1192
+
1193
+ ### Coordination:
1194
+ - **ruflo__coordination_topology** — View/change coordination topology.
1195
+ - **ruflo__coordination_load_balance** — Distribute work across agents.
1196
+ - **ruflo__coordination_orchestrate** — Orchestrate complex multi-agent tasks.
1197
+ - **ruflo__coordination_sync** — Synchronize state across agents.
1198
+
1199
+ ### Session Management:
1200
+ - **ruflo__session_save** — Save current session state.
1201
+ - **ruflo__session_restore** — Restore a previous session.
1202
+ - **ruflo__session_list** — List available sessions.
1203
+
1204
+ ## Group 4: Memory & Knowledge (ruflo)
1205
+ Persistent memory, vector search, embeddings, and pattern storage. ${TOOL_GROUPS.memory.enabled ? "ACTIVE" : "DISABLED"}
1206
+
1207
+ ### Memory Operations:
1208
+ - **ruflo__memory_store** — Store data in persistent memory.
1209
+ \`{"key": "my-key", "value": "data to store", "namespace": "default", "tags": ["tag1"]}\`
1210
+ - **ruflo__memory_retrieve** — Get stored data by key. \`{"key": "my-key"}\`
1211
+ - **ruflo__memory_search** — Semantic vector search across stored memories.
1212
+ \`{"query": "what to search for", "limit": 5, "namespace": "default"}\`
1213
+ - **ruflo__memory_list** — List all stored keys in a namespace.
1214
+ - **ruflo__memory_delete** — Remove a stored memory.
1215
+ - **ruflo__memory_stats** — View memory usage statistics.
1216
+
1217
+ ### Embeddings:
1218
+ - **ruflo__embeddings_generate** — Generate vector embeddings for text.
1219
+ - **ruflo__embeddings_compare** — Compare semantic similarity of two texts.
1220
+ - **ruflo__embeddings_search** — Search embeddings database by similarity.
1221
+ - **ruflo__embeddings_neural** — Generate neural embeddings.
1222
+ - **ruflo__embeddings_hyperbolic** — Generate hyperbolic embeddings for hierarchical data.
1223
+
1224
+ ### AgentDB (Advanced Pattern Storage):
1225
+ - **ruflo__agentdb_pattern-store** — Store a learned pattern with metadata.
1226
+ \`{"pattern": "description", "category": "code|debug|architecture", "confidence": 0.9}\`
1227
+ - **ruflo__agentdb_pattern-search** — Search patterns by similarity.
1228
+ - **ruflo__agentdb_route** — Route a query to the most relevant stored pattern.
1229
+ - **ruflo__agentdb_feedback** — Provide feedback on a pattern (reinforcement learning).
1230
+ - **ruflo__agentdb_context-synthesize** — Synthesize context from multiple sources.
1231
+ - **ruflo__agentdb_semantic-route** — Semantic routing based on stored knowledge.
1232
+ - **ruflo__agentdb_consolidate** — Consolidate and deduplicate stored patterns.
1233
+ - **ruflo__agentdb_batch** — Batch operations on patterns.
1234
+ - **ruflo__agentdb_session-start** / **ruflo__agentdb_session-end** — Session tracking.
1235
+ - **ruflo__agentdb_hierarchical-store** / **ruflo__agentdb_hierarchical-recall** — Hierarchical memory.
1236
+
1237
+ ## Group 5: Dev Tools & Analysis (ruflo)
1238
+ Performance, system health, GitHub integration, code analysis, terminal. ${TOOL_GROUPS.devtools.enabled ? "ACTIVE" : "DISABLED"}
1239
+
1240
+ ### System & Performance:
1241
+ - **ruflo__system_status** — System health overview.
1242
+ - **ruflo__system_metrics** — Detailed performance metrics.
1243
+ - **ruflo__system_health** — Health check across all subsystems.
1244
+ - **ruflo__performance_report** — Generate performance report.
1245
+ - **ruflo__performance_bottleneck** — Identify performance bottlenecks.
1246
+ - **ruflo__performance_benchmark** — Run benchmarks.
1247
+ - **ruflo__performance_optimize** — Get optimization recommendations.
1248
+ - **ruflo__performance_profile** — Profile specific operations.
1249
+
1250
+ ### Code Analysis:
1251
+ - **ruflo__analyze_diff** — Analyze a code diff.
1252
+ - **ruflo__analyze_diff-risk** — Assess risk level of changes.
1253
+ - **ruflo__analyze_diff-classify** — Classify type of changes (feature, bugfix, refactor).
1254
+ - **ruflo__analyze_diff-reviewers** — Suggest code reviewers.
1255
+ - **ruflo__analyze_file-risk** — Assess risk of a specific file.
1256
+
1257
+ ### GitHub Integration:
1258
+ - **ruflo__github_repo_analyze** — Analyze a GitHub repository.
1259
+ \`{"repo": "owner/repo", "analysis_type": "code_quality|performance|security"}\`
1260
+ - **ruflo__github_pr_manage** — Manage pull requests (create, review, merge).
1261
+ - **ruflo__github_issue_track** — Track and manage issues.
1262
+ - **ruflo__github_workflow** — Manage GitHub Actions workflows.
1263
+ - **ruflo__github_metrics** — Repository metrics and insights.
1264
+
1265
+ ### Terminal Access:
1266
+ - **ruflo__terminal_create** — Create a terminal session.
1267
+ - **ruflo__terminal_execute** — Execute a command. \`{"command": "ls -la"}\`
1268
+ - **ruflo__terminal_list** — List active terminals.
1269
+ - **ruflo__terminal_history** — View command history.
1270
+
1271
+ ### Development Hooks:
1272
+ - **ruflo__hooks_pre-task** / **ruflo__hooks_post-task** — Task lifecycle hooks for learning.
1273
+ - **ruflo__hooks_pre-edit** / **ruflo__hooks_post-edit** — File edit hooks.
1274
+ - **ruflo__hooks_session-start** / **ruflo__hooks_session-end** — Session lifecycle.
1275
+ - **ruflo__hooks_worker-dispatch** — Dispatch background workers.
1276
+ Workers: \`optimize\`, \`audit\`, \`testgaps\`, \`document\`, \`map\`, \`deepdive\`, \`benchmark\`
1277
+ - **ruflo__hooks_model-route** — Route to optimal AI model for a task.
1278
+ - **ruflo__hooks_explain** — Explain a routing or intelligence decision.
1279
+
1280
+ ### Configuration:
1281
+ - **ruflo__config_get** / **ruflo__config_set** / **ruflo__config_list** — Manage settings.
1282
+
1283
+ ### Progress Tracking:
1284
+ - **ruflo__progress_check** — Check implementation progress.
1285
+ - **ruflo__progress_summary** — Summarize overall progress.
1286
+
1287
+ # Decision Framework
1288
+
1289
+ When the user asks you something, follow this decision tree:
1290
+
1291
+ 1. **Knowledge question** ("how do I...", "what is...") → \`search\` first, then \`web_research\` if not found
1292
+ 2. **Research request** ("look up", "compare", "find out") → \`web_research\` with appropriate action (use \`goap\` for important questions)
1293
+ 3. **Code task** ("write", "fix", "implement") → \`ruvector__hooks_route\` to find best approach, then \`ruflo__agent_spawn\`
1294
+ 4. **Analysis request** ("analyze", "review", "audit") → spawn reviewer/security agents + analysis tools
1295
+ 5. **Multi-step project** → \`ruflo__task_create\` for tracking, \`ruflo__swarm_init\` for coordination
1296
+ 6. **Memory/recall** ("remember", "save", "what did we...") → \`ruflo__memory_store\` / \`ruflo__memory_search\`
1297
+ 7. **System question** ("what tools", "help") → \`guidance(topic='overview')\`
1298
+ 8. **Performance concern** → \`ruflo__performance_bottleneck\` + \`ruflo__performance_optimize\`
1299
+ 9. **GitHub task** → \`ruflo__github_*\` tools
1300
+ 10. **Unknown** → \`guidance(topic='overview')\` to discover capabilities
1301
+
1302
+ # Execution Patterns
1303
+
1304
+ ### Simple Question
1305
+ \`search\` or \`web_research\` → synthesize → respond
1306
+
1307
+ ### Complex Research
1308
+ \`web_research(action='goap')\` → analyze → respond with citations
1309
+
1310
+ ### Code Implementation
1311
+ \`ruvector__hooks_route\` → \`ruflo__agent_spawn(coder)\` → track with \`ruflo__task_create\` → report
1312
+
1313
+ ### Multi-Agent Analysis
1314
+ \`ruflo__swarm_init(hierarchical)\` → spawn agents → coordinate → synthesize results
1315
+
1316
+ ### Learning & Memory
1317
+ \`ruflo__memory_search\` (check existing) → do work → \`ruflo__memory_store\` (save results) → \`ruvector__hooks_learn\`
1318
+
1319
+ # Parallel Execution
1320
+
1321
+ When multiple independent tools can help, call them ALL in parallel:
1322
+ - Search + web_research simultaneously
1323
+ - Spawn multiple agents at once (coder + tester + reviewer)
1324
+ - Run analysis + performance + security tools in parallel
1325
+ NEVER call tools sequentially when they could run in parallel.
1326
+
1327
+ # Response Rules
1328
+
1329
+ 1. **Call tools FIRST**, then present results conversationally — NEVER show raw JSON to the user
1330
+ 2. Use markdown: **bold** headers, bullet points, numbered steps, tables for comparisons
1331
+ 3. Synthesize tool results naturally — be a helpful colleague, not a data pipe
1332
+ 4. Cite sources when available from web_research results
1333
+ 5. If a tool fails, say so honestly and try an alternative approach
1334
+ 6. For complex tasks, briefly outline your plan before executing
1335
+ 7. After completing work, suggest relevant follow-up actions
1336
+ 8. When spawning agents, explain what each agent will do
1337
+
1338
+ # Never Expose to User
1339
+
1340
+ - Raw JSON, similarity scores, chunk IDs, internal IDs, task IDs
1341
+ - Tool names, function names, API endpoints, backend names
1342
+ - References to "MCP", "tool calls", "vectors", "embeddings", infrastructure
1343
+ - The prefixes "ruflo__" or "ruvector__" — just describe what you're doing naturally
1344
+ - Error stack traces — summarize errors in plain language`;
1345
+ }
1346
+
1347
+ // =============================================================================
1348
+ // AUTOPILOT MODE — Server-side auto-continue loop (ADR-037)
1349
+ // =============================================================================
1350
+
1351
+ const detailStore = new Map(); // detailToken → full tool result (TTL: 5min)
1352
+
1353
+ const AUTOPILOT_SYSTEM_PROMPT = `
1354
+ You are in AUTOPILOT MODE. You should:
1355
+ 1. Break complex tasks into steps and execute them using available tools
1356
+ 2. Call MULTIPLE tools in parallel when they are independent
1357
+ 3. After each tool result, analyze it and decide the next action
1358
+ 4. Continue until the task is complete — do NOT ask the user for confirmation
1359
+ 5. Use memory_search to find relevant patterns before starting
1360
+ 6. Summarize your progress at each step
1361
+ 7. When done, provide a final summary of everything accomplished
1362
+
1363
+ Parallel execution patterns:
1364
+ - Research: memory_search + hooks_route + agent_spawn(researcher) — all in parallel
1365
+ - Code: agent_spawn(coder) + agent_spawn(tester) — parallel, then review
1366
+ - Analysis: search multiple sources in parallel → synthesize → report
1367
+ - Security: security_scan + hooks_route(audit) + memory_search(CVEs) — parallel
1368
+ `;
1369
+
1370
+ const AUTOPILOT_BLOCKED_PATTERNS = [
1371
+ /^deploy_/,
1372
+ /^security_delete/,
1373
+ /^browser_fill$/,
1374
+ /^browser_click$/,
1375
+ /terminal_execute/,
1376
+ ];
1377
+
1378
+ function isBlockedTool(name) {
1379
+ return AUTOPILOT_BLOCKED_PATTERNS.some(p => p.test(name));
1380
+ }
1381
+
1382
+ function sendAutopilotEvent(res, data) {
1383
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
1384
+ }
1385
+
1386
+ function safeParseArgs(args) {
1387
+ if (typeof args === 'object' && args !== null) return args;
1388
+ try { return JSON.parse(args || '{}'); } catch { return {}; }
1389
+ }
1390
+
1391
+ function autopilotSleep(ms) {
1392
+ return new Promise(resolve => setTimeout(resolve, ms));
1393
+ }
1394
+
1395
+ async function handleAutopilot(req, res, provider, body) {
1396
+ const maxSteps = Math.min(parseInt(req.headers['x-autopilot-max-steps'] || '20', 10), 50);
1397
+ const cooldownMs = parseInt(process.env.AUTOPILOT_COOLDOWN || '500', 10);
1398
+ const stepTimeoutMs = parseInt(process.env.AUTOPILOT_STEP_TIMEOUT || '30000', 10);
1399
+
1400
+ // SSE setup
1401
+ res.setHeader('Content-Type', 'text/event-stream');
1402
+ res.setHeader('Cache-Control', 'no-cache');
1403
+ res.setHeader('Connection', 'keep-alive');
1404
+ res.setHeader('X-Accel-Buffering', 'no');
1405
+
1406
+ let messages = [...body.messages];
1407
+ let step = 0;
1408
+ let aborted = false;
1409
+ let totalTasks = 0;
1410
+ const startTime = Date.now();
1411
+
1412
+ req.on('close', () => { aborted = true; });
1413
+
1414
+ sendAutopilotEvent(res, { type: 'autopilot_start', maxSteps });
1415
+
1416
+ // Get the tools list for the AI provider (OpenAI function calling format)
1417
+ const allTools = [...BUILTIN_TOOLS, ...getActiveTools()];
1418
+ const toolDefs = allTools.map(t => ({
1419
+ type: 'function',
1420
+ function: {
1421
+ name: t.name,
1422
+ description: t.description || '',
1423
+ parameters: t.inputSchema || { type: 'object', properties: {} },
1424
+ },
1425
+ }));
1426
+
1427
+ while (step < maxSteps && !aborted) {
1428
+ // 1. Call upstream AI provider (non-streaming for tool call parsing)
1429
+ const apiKey = provider.getKey();
1430
+ let aiResult;
1431
+ try {
1432
+ const aiResponse = await fetch(provider.baseURL, {
1433
+ method: 'POST',
1434
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
1435
+ body: JSON.stringify({
1436
+ ...body,
1437
+ messages,
1438
+ stream: false,
1439
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
1440
+ }),
1441
+ signal: AbortSignal.timeout(stepTimeoutMs),
1442
+ });
1443
+ aiResult = await aiResponse.json();
1444
+ } catch (err) {
1445
+ sendAutopilotEvent(res, { type: 'autopilot_error', error: `AI call failed: ${err.message}` });
1446
+ break;
1447
+ }
1448
+
1449
+ const choice = aiResult.choices?.[0];
1450
+ if (!choice) {
1451
+ sendAutopilotEvent(res, { type: 'autopilot_error', error: 'No response from AI' });
1452
+ break;
1453
+ }
1454
+
1455
+ // 2. Check for tool calls
1456
+ const toolCalls = choice.message?.tool_calls;
1457
+
1458
+ if (!toolCalls || toolCalls.length === 0) {
1459
+ // Final text response — send it
1460
+ sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message?.content || '' });
1461
+ break;
1462
+ }
1463
+
1464
+ // 3. Execute ALL tool calls in parallel
1465
+ step++;
1466
+ const groupId = `g${step}`;
1467
+ const taskEvents = toolCalls.map((tc, i) => ({
1468
+ taskId: `t${totalTasks + i + 1}`,
1469
+ tool: tc.function.name,
1470
+ args: safeParseArgs(tc.function.arguments),
1471
+ status: 'running',
1472
+ }));
1473
+ totalTasks += taskEvents.length;
1474
+
1475
+ // If the AI also included text content, stream it before tools
1476
+ if (choice.message?.content) {
1477
+ sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message.content });
1478
+ }
1479
+
1480
+ // Stream group start
1481
+ sendAutopilotEvent(res, { type: 'task_group_start', groupId, step, tasks: taskEvents });
1482
+
1483
+ // Append assistant message to conversation
1484
+ messages.push(choice.message);
1485
+
1486
+ // Execute tools in parallel
1487
+ const groupStart = Date.now();
1488
+ const results = await Promise.allSettled(
1489
+ toolCalls.map(async (tc, i) => {
1490
+ const taskId = taskEvents[i].taskId;
1491
+ const toolName = tc.function.name;
1492
+ const toolArgs = safeParseArgs(tc.function.arguments);
1493
+ const taskStart = Date.now();
1494
+
1495
+ // Check blocklist
1496
+ if (isBlockedTool(toolName)) {
1497
+ sendAutopilotEvent(res, {
1498
+ type: 'task_update', taskId, status: 'blocked',
1499
+ summary: `${toolName} requires confirmation`,
1500
+ duration: Date.now() - taskStart,
1501
+ });
1502
+ return { toolCallId: tc.id, blocked: true, toolName };
1503
+ }
1504
+
1505
+ try {
1506
+ const result = await executeTool(toolName, toolArgs);
1507
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1508
+
1509
+ // Store full detail, generate token for lazy loading
1510
+ const detailToken = `dt_${taskId}_${Date.now()}`;
1511
+ detailStore.set(detailToken, resultStr);
1512
+
1513
+ // Stream task completion with summary only
1514
+ const summary = resultStr.length > 120
1515
+ ? resultStr.substring(0, 120).replace(/\n/g, ' ') + '...'
1516
+ : resultStr.replace(/\n/g, ' ');
1517
+
1518
+ sendAutopilotEvent(res, {
1519
+ type: 'task_update', taskId, status: 'completed',
1520
+ summary, duration: Date.now() - taskStart, detailToken,
1521
+ });
1522
+
1523
+ return { toolCallId: tc.id, content: resultStr };
1524
+ } catch (err) {
1525
+ sendAutopilotEvent(res, {
1526
+ type: 'task_update', taskId, status: 'failed',
1527
+ summary: err.message, duration: Date.now() - taskStart,
1528
+ });
1529
+ return { toolCallId: tc.id, content: `Error: ${err.message}` };
1530
+ }
1531
+ })
1532
+ );
1533
+
1534
+ // Stream group end
1535
+ sendAutopilotEvent(res, { type: 'task_group_end', groupId, step, duration: Date.now() - groupStart });
1536
+
1537
+ // Check if any tools were blocked — pause autopilot
1538
+ const blockedResults = results
1539
+ .filter(r => r.status === 'fulfilled' && r.value.blocked)
1540
+ .map(r => r.value);
1541
+ if (blockedResults.length > 0) {
1542
+ sendAutopilotEvent(res, {
1543
+ type: 'autopilot_paused',
1544
+ reason: 'blocked_tools',
1545
+ tools: blockedResults.map(b => b.toolName),
1546
+ });
1547
+ break;
1548
+ }
1549
+
1550
+ // Append tool results to messages
1551
+ for (const r of results) {
1552
+ if (r.status === 'fulfilled' && !r.value.blocked) {
1553
+ messages.push({
1554
+ role: 'tool',
1555
+ tool_call_id: r.value.toolCallId,
1556
+ content: r.value.content,
1557
+ });
1558
+ }
1559
+ }
1560
+
1561
+ // Cooldown to prevent runaway
1562
+ await autopilotSleep(cooldownMs);
1563
+ }
1564
+
1565
+ if (step >= maxSteps && !aborted) {
1566
+ sendAutopilotEvent(res, {
1567
+ type: 'autopilot_text',
1568
+ content: `\n⚠️ Autopilot reached max steps (${maxSteps}). Stopping.\n`,
1569
+ });
1570
+ }
1571
+
1572
+ sendAutopilotEvent(res, {
1573
+ type: 'autopilot_end',
1574
+ totalSteps: step,
1575
+ totalTasks,
1576
+ duration: Date.now() - startTime,
1577
+ });
1578
+
1579
+ res.write('data: [DONE]\n\n');
1580
+ res.end();
1581
+
1582
+ // Clean up detail store after 5 minutes
1583
+ const detailTTL = parseInt(process.env.AUTOPILOT_DETAIL_TTL || '300000', 10);
1584
+ setTimeout(() => {
1585
+ for (const [key] of detailStore) {
1586
+ if (key.startsWith('dt_')) detailStore.delete(key);
1587
+ }
1588
+ }, detailTTL);
1589
+ }
1590
+
1591
+ // Lazy detail loading endpoint
1592
+ app.get('/autopilot/detail/:token', (req, res) => {
1593
+ const content = detailStore.get(req.params.token);
1594
+ if (content) {
1595
+ res.json({ content });
1596
+ } else {
1597
+ res.status(404).json({ error: 'Detail expired or not found' });
1598
+ }
1599
+ });
1600
+
1601
+ // =============================================================================
1602
+ // CHAT COMPLETIONS PROXY
1603
+ // =============================================================================
1604
+
1605
+ app.post("/chat/completions", async (req, res) => {
1606
+ const model = req.body?.model;
1607
+ const providerName = resolveProvider(model);
1608
+ const provider = PROVIDER_ROUTES[providerName];
1609
+ const apiKey = provider.getKey();
1610
+
1611
+ if (!apiKey) return res.status(401).json({ error: { message: `No API key for provider: ${providerName}` } });
1612
+
1613
+ // Inject comprehensive system prompt as the first message
1614
+ const body = { ...req.body };
1615
+ if (body.messages && Array.isArray(body.messages)) {
1616
+ let systemPrompt = buildSystemPrompt();
1617
+ // Add autopilot instructions if autopilot mode is active
1618
+ const isAutopilot = req.headers['x-autopilot'] === 'true';
1619
+ if (isAutopilot) {
1620
+ systemPrompt = AUTOPILOT_SYSTEM_PROMPT + '\n\n' + systemPrompt;
1621
+ }
1622
+ // Prepend our system prompt before any existing messages
1623
+ const hasSystemMsg = body.messages[0]?.role === "system";
1624
+ if (hasSystemMsg) {
1625
+ // Merge with existing system message
1626
+ body.messages = [
1627
+ { role: "system", content: systemPrompt + "\n\n" + body.messages[0].content },
1628
+ ...body.messages.slice(1),
1629
+ ];
1630
+ } else {
1631
+ body.messages = [{ role: "system", content: systemPrompt }, ...body.messages];
1632
+ }
1633
+ }
1634
+
1635
+ // Route to autopilot handler if x-autopilot header is set
1636
+ if (req.headers['x-autopilot'] === 'true') {
1637
+ return handleAutopilot(req, res, provider, body);
1638
+ }
1639
+
1640
+ try {
1641
+ const upstream = await fetch(provider.baseURL, {
1642
+ method: "POST",
1643
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1644
+ body: JSON.stringify(body),
1645
+ });
1646
+
1647
+ if (!upstream.ok) {
1648
+ const errBody = await upstream.text();
1649
+ console.error(`Proxy error [${providerName}/${model}]: ${upstream.status} ${errBody.substring(0, 200)}`);
1650
+ // Normalize all upstream errors into OpenAI-compatible format so the
1651
+ // Chat UI's OpenAI SDK can parse them instead of "400 (no body)".
1652
+ let errorMessage = `Upstream ${providerName} error (${upstream.status})`;
1653
+ try {
1654
+ const parsed = JSON.parse(errBody);
1655
+ // Gemini returns [{"error": {"message": "..."}}]
1656
+ if (Array.isArray(parsed) && parsed[0]?.error?.message) {
1657
+ errorMessage = parsed[0].error.message;
1658
+ // OpenAI/OpenRouter return {"error": {"message": "..."}}
1659
+ } else if (parsed.error?.message) {
1660
+ errorMessage = parsed.error.message;
1661
+ }
1662
+ } catch {}
1663
+ return res.status(upstream.status).json({
1664
+ error: { message: errorMessage, type: "upstream_error", code: upstream.status },
1665
+ });
1666
+ }
1667
+
1668
+ res.setHeader("Content-Type", upstream.headers.get("content-type") || "application/json");
1669
+
1670
+ if (req.body?.stream && upstream.body) {
1671
+ const reader = upstream.body.getReader();
1672
+ const decoder = new TextDecoder();
1673
+ try {
1674
+ while (true) {
1675
+ const { done, value } = await reader.read();
1676
+ if (done) break;
1677
+ res.write(decoder.decode(value, { stream: true }));
1678
+ }
1679
+ } catch (e) { /* stream closed */ }
1680
+ finally { res.end(); }
1681
+ } else {
1682
+ res.send(await upstream.text());
1683
+ }
1684
+ } catch (err) {
1685
+ console.error(`Proxy error [${providerName}/${model}]:`, err.message);
1686
+ res.status(502).json({ error: { message: `Upstream error: ${err.message}` } });
1687
+ }
1688
+ });
1689
+
1690
+ // =============================================================================
1691
+ // MODELS & HEALTH
1692
+ // =============================================================================
1693
+
1694
+ const KNOWN_MODELS = [
1695
+ "gemini-2.5-pro", "gemini-2.5-flash",
1696
+ "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini",
1697
+ "o3-mini", "o1-mini",
1698
+ ];
1699
+
1700
+ app.get("/models", (_, res) => {
1701
+ res.json({ object: "list", data: KNOWN_MODELS.map(id => ({ id, object: "model", owned_by: "system" })) });
1702
+ });
1703
+
1704
+ app.get("/health", (_, res) => {
1705
+ const backends = {};
1706
+ for (const [name, client] of mcpBackends) {
1707
+ backends[name] = { ready: client.ready, tools: client.tools.length };
1708
+ }
1709
+ const activeTools = getActiveTools();
1710
+ const groups = {};
1711
+ for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1712
+ groups[name] = { enabled: g.enabled, source: g.source };
1713
+ }
1714
+ res.json({
1715
+ status: "ok", service: "mcp-bridge", version: "2.0.0",
1716
+ tools: { builtin: BUILTIN_TOOLS.length, external: activeTools.length, total: BUILTIN_TOOLS.length + activeTools.length },
1717
+ groups, backends,
1718
+ });
1719
+ });
1720
+
1721
+ // GET /groups — list tool groups and their status
1722
+ app.get("/groups", (_, res) => {
1723
+ const activeTools = getActiveTools();
1724
+ const result = {};
1725
+ for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1726
+ const tools = name === "core" ? BUILTIN_TOOLS :
1727
+ activeTools.filter(t => {
1728
+ if (g.source !== t._backend) return false;
1729
+ if (!g.prefixes) return true;
1730
+ return g.prefixes.some(p => t._originalName.startsWith(p));
1731
+ });
1732
+ result[name] = {
1733
+ enabled: g.enabled,
1734
+ description: g.description,
1735
+ tools: tools.length,
1736
+ toolNames: tools.map(t => t.name).slice(0, 10),
1737
+ };
1738
+ }
1739
+ res.json(result);
1740
+ });
1741
+
1742
+ // =============================================================================
1743
+ // STARTUP
1744
+ // =============================================================================
1745
+
1746
+ async function main() {
1747
+ const isPublic = BIND_HOST !== "127.0.0.1" && BIND_HOST !== "localhost";
1748
+ if (isPublic && !process.env.MCP_AUTH_TOKEN) {
1749
+ console.error(
1750
+ "FATAL: refusing to bind a public interface without MCP_AUTH_TOKEN. " +
1751
+ "Generate one with: MCP_AUTH_TOKEN=$(openssl rand -base64 32)"
1752
+ );
1753
+ process.exit(1);
1754
+ }
1755
+ app.listen(PORT, BIND_HOST, () => {
1756
+ console.log(`MCP Bridge v2.0.0 on port ${PORT} (${BIND_HOST})`);
1757
+ const enabled = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled).map(([n]) => n);
1758
+ console.log(`Active groups: ${enabled.join(", ")}`);
1759
+ // ADR-166 §6 — startup posture banner
1760
+ console.log(
1761
+ `[security] bind=${BIND_HOST} auth=${process.env.MCP_AUTH_TOKEN ? "bearer" : "off (local-only)"} ` +
1762
+ `terminal=${MCP_ENABLE_TERMINAL ? "ENABLED (⚠ opt-in)" : "disabled"}`,
1763
+ );
1764
+ if (MCP_ENABLE_TERMINAL) {
1765
+ console.warn(
1766
+ "[security] WARNING: terminal_execute is enabled. This tool grants shell access " +
1767
+ "inside the bridge container to any client the auth layer accepts. Ensure " +
1768
+ "MCP_AUTH_TOKEN is set on any non-loopback bind. See ADR-166 §6 Phase 1d.",
1769
+ );
1770
+ }
1771
+ });
1772
+
1773
+ const anyBackendNeeded = BACKEND_DEFS.some(isBackendNeeded);
1774
+ if (anyBackendNeeded) {
1775
+ console.log("Initializing MCP backends...");
1776
+ await initBackends();
1777
+ }
1778
+ }
1779
+
1780
+ main().catch(err => { console.error("Fatal:", err); process.exit(1); });