ruflo 3.42.3 → 3.42.5
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.
- package/README.md +422 -422
- package/bin/ruflo.js +77 -77
- package/package.json +117 -117
- package/src/chat-ui/Dockerfile +25 -25
- package/src/chat-ui/patch-mcp-url-safety.sh +28 -28
- package/src/config/config.example.json +76 -76
- package/src/mcp-bridge/Dockerfile +47 -47
- package/src/mcp-bridge/index.js +1780 -1780
- package/src/mcp-bridge/mcp-stdio-kernel.js +159 -159
- package/src/mcp-bridge/package.json +17 -17
- package/src/mcp-bridge/test-harness.js +470 -470
- package/src/mcp-bridge/test-runtime-security.mjs +208 -208
- package/src/mcp-bridge/test-security-lock.js +121 -121
- package/src/nginx/Dockerfile +10 -10
- package/src/nginx/nginx.conf +67 -67
- package/src/nginx/static/favicon-dark.svg +4 -4
- package/src/nginx/static/favicon.svg +4 -4
- package/src/nginx/static/icon.svg +5 -5
- package/src/nginx/static/logo.svg +9 -9
- package/src/nginx/static/manifest.json +22 -22
- package/src/nginx/static/welcome.js +184 -184
- package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
- package/src/ruvocal/.devcontainer/Dockerfile +9 -9
- package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
- package/src/ruvocal/.dockerignore +16 -16
- package/src/ruvocal/.eslintignore +13 -13
- package/src/ruvocal/.eslintrc.cjs +45 -45
- package/src/ruvocal/.gcloudignore +18 -18
- package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
- package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
- package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
- package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
- package/src/ruvocal/.github/release.yml +16 -16
- package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
- package/src/ruvocal/.github/workflows/build-image.yml +142 -142
- package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
- package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
- package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
- package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
- package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
- package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
- package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
- package/src/ruvocal/.husky/lint-stage-config.js +4 -4
- package/src/ruvocal/.husky/pre-commit +2 -2
- package/src/ruvocal/.prettierignore +14 -14
- package/src/ruvocal/.prettierrc +7 -7
- package/src/ruvocal/CLAUDE.md +126 -126
- package/src/ruvocal/Dockerfile +96 -96
- package/src/ruvocal/LICENSE +202 -202
- package/src/ruvocal/PRIVACY.md +41 -41
- package/src/ruvocal/README.md +164 -164
- package/src/ruvocal/chart/Chart.yaml +5 -5
- package/src/ruvocal/chart/env/dev.yaml +260 -260
- package/src/ruvocal/chart/env/prod.yaml +273 -273
- package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
- package/src/ruvocal/chart/templates/config.yaml +10 -10
- package/src/ruvocal/chart/templates/deployment.yaml +81 -81
- package/src/ruvocal/chart/templates/hpa.yaml +45 -45
- package/src/ruvocal/chart/templates/infisical.yaml +24 -24
- package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
- package/src/ruvocal/chart/templates/ingress.yaml +32 -32
- package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
- package/src/ruvocal/chart/templates/service-account.yaml +13 -13
- package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
- package/src/ruvocal/chart/templates/service.yaml +21 -21
- package/src/ruvocal/chart/values.yaml +73 -73
- package/src/ruvocal/cloudbuild.yaml +68 -68
- package/src/ruvocal/config/branding.env.example +19 -19
- package/src/ruvocal/docker-compose.yml +21 -21
- package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
- package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
- package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
- package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
- package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
- package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
- package/src/ruvocal/docs/source/_toctree.yml +30 -30
- package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
- package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
- package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
- package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
- package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
- package/src/ruvocal/docs/source/configuration/overview.md +89 -89
- package/src/ruvocal/docs/source/configuration/theming.md +20 -20
- package/src/ruvocal/docs/source/developing/architecture.md +48 -48
- package/src/ruvocal/docs/source/index.md +53 -53
- package/src/ruvocal/docs/source/installation/docker.md +43 -43
- package/src/ruvocal/docs/source/installation/helm.md +43 -43
- package/src/ruvocal/docs/source/installation/local.md +62 -62
- package/src/ruvocal/entrypoint.sh +18 -18
- package/src/ruvocal/mcp-bridge/Dockerfile +47 -47
- package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
- package/src/ruvocal/mcp-bridge/index.js +1995 -1995
- package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
- package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
- package/src/ruvocal/mcp-bridge/package.json +17 -17
- package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
- package/src/ruvocal/package-lock.json +11741 -11741
- package/src/ruvocal/package.json +121 -121
- package/src/ruvocal/postcss.config.js +6 -6
- package/src/ruvocal/rvf.manifest.json +204 -204
- package/src/ruvocal/scripts/config.ts +64 -64
- package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
- package/src/ruvocal/scripts/populate.ts +288 -288
- package/src/ruvocal/scripts/samples.txt +194 -194
- package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
- package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
- package/src/ruvocal/src/ambient.d.ts +7 -7
- package/src/ruvocal/src/app.d.ts +29 -29
- package/src/ruvocal/src/app.html +53 -53
- package/src/ruvocal/src/hooks.server.ts +32 -32
- package/src/ruvocal/src/hooks.ts +6 -6
- package/src/ruvocal/src/lib/APIClient.ts +148 -148
- package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
- package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
- package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
- package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
- package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
- package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
- package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
- package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
- package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
- package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
- package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
- package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
- package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
- package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
- package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
- package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
- package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
- package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
- package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
- package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
- package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
- package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
- package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
- package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
- package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
- package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
- package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
- package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
- package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
- package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
- package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
- package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
- package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
- package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
- package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
- package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
- package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
- package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
- package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
- package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
- package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
- package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
- package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
- package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
- package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
- package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
- package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
- package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
- package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
- package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
- package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
- package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
- package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
- package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
- package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
- package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
- package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
- package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
- package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
- package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
- package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
- package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
- package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
- package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
- package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
- package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
- package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
- package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
- package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
- package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
- package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
- package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
- package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
- package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
- package/src/ruvocal/src/lib/constants/mime.ts +11 -11
- package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
- package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
- package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
- package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
- package/src/ruvocal/src/lib/createShareLink.ts +27 -27
- package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
- package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
- package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
- package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
- package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
- package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
- package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
- package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
- package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
- package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
- package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
- package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
- package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
- package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
- package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
- package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
- package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
- package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
- package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
- package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
- package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
- package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
- package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
- package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
- package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
- package/src/ruvocal/src/lib/server/api/types.ts +37 -37
- package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
- package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
- package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
- package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
- package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
- package/src/ruvocal/src/lib/server/auth.ts +554 -554
- package/src/ruvocal/src/lib/server/config.ts +187 -187
- package/src/ruvocal/src/lib/server/conversation.ts +83 -83
- package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
- package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
- package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
- package/src/ruvocal/src/lib/server/database.ts +145 -145
- package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
- package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
- package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
- package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
- package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
- package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
- package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
- package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
- package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
- package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
- package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
- package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
- package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
- package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
- package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
- package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
- package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
- package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
- package/src/ruvocal/src/lib/server/logger.ts +42 -42
- package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
- package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
- package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
- package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
- package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
- package/src/ruvocal/src/lib/server/metrics.ts +255 -255
- package/src/ruvocal/src/lib/server/models.ts +518 -518
- package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
- package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
- package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
- package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
- package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
- package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
- package/src/ruvocal/src/lib/server/router/types.ts +21 -21
- package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
- package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
- package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
- package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
- package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
- package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
- package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
- package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
- package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
- package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
- package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
- package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
- package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
- package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
- package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
- package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
- package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
- package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
- package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
- package/src/ruvocal/src/lib/stores/errors.ts +9 -9
- package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
- package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
- package/src/ruvocal/src/lib/stores/loading.ts +3 -3
- package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
- package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
- package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
- package/src/ruvocal/src/lib/stores/settings.ts +182 -182
- package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
- package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
- package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
- package/src/ruvocal/src/lib/switchTheme.ts +124 -124
- package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
- package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
- package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
- package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
- package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
- package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
- package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
- package/src/ruvocal/src/lib/types/Message.ts +41 -41
- package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
- package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
- package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
- package/src/ruvocal/src/lib/types/Model.ts +23 -23
- package/src/ruvocal/src/lib/types/Report.ts +12 -12
- package/src/ruvocal/src/lib/types/Review.ts +6 -6
- package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
- package/src/ruvocal/src/lib/types/Session.ts +22 -22
- package/src/ruvocal/src/lib/types/Settings.ts +93 -93
- package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
- package/src/ruvocal/src/lib/types/Template.ts +6 -6
- package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
- package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
- package/src/ruvocal/src/lib/types/Tool.ts +77 -77
- package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
- package/src/ruvocal/src/lib/types/User.ts +14 -14
- package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
- package/src/ruvocal/src/lib/utils/auth.ts +17 -17
- package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
- package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
- package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
- package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
- package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
- package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
- package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
- package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
- package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
- package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
- package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
- package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
- package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
- package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
- package/src/ruvocal/src/lib/utils/hf.ts +17 -17
- package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
- package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
- package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
- package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
- package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
- package/src/ruvocal/src/lib/utils/marked.ts +531 -531
- package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
- package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
- package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +286 -286
- package/src/ruvocal/src/lib/utils/messageUpdates.ts +332 -332
- package/src/ruvocal/src/lib/utils/mime.ts +56 -56
- package/src/ruvocal/src/lib/utils/models.ts +14 -14
- package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
- package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
- package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
- package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
- package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
- package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
- package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
- package/src/ruvocal/src/lib/utils/sum.ts +3 -3
- package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
- package/src/ruvocal/src/lib/utils/template.ts +53 -53
- package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
- package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
- package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
- package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
- package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
- package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
- package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
- package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
- package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
- package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
- package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
- package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
- package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
- package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
- package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
- package/src/ruvocal/src/lib/utils/updates.ts +39 -39
- package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
- package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
- package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
- package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
- package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
- package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
- package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
- package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
- package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
- package/src/ruvocal/src/routes/+error.svelte +20 -20
- package/src/ruvocal/src/routes/+layout.svelte +324 -324
- package/src/ruvocal/src/routes/+layout.ts +91 -91
- package/src/ruvocal/src/routes/+page.svelte +168 -168
- package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
- package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
- package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
- package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
- package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
- package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
- package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
- package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
- package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
- package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
- package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
- package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
- package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
- package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
- package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
- package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
- package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
- package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
- package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
- package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
- package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
- package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
- package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
- package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
- package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
- package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
- package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
- package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
- package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
- package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
- package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
- package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
- package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +593 -593
- package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
- package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
- package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
- package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
- package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
- package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
- package/src/ruvocal/src/routes/login/+server.ts +5 -5
- package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
- package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
- package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
- package/src/ruvocal/src/routes/logout/+server.ts +18 -18
- package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
- package/src/ruvocal/src/routes/models/+page.svelte +233 -233
- package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
- package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
- package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
- package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
- package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
- package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
- package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
- package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
- package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
- package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
- package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
- package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
- package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
- package/src/ruvocal/src/styles/highlight-js.css +195 -195
- package/src/ruvocal/src/styles/main.css +144 -144
- package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
- package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
- package/src/ruvocal/static/chatui/favicon.svg +3 -3
- package/src/ruvocal/static/chatui/icon.svg +3 -3
- package/src/ruvocal/static/chatui/logo.svg +7 -7
- package/src/ruvocal/static/chatui/manifest.json +54 -54
- package/src/ruvocal/static/chatui/welcome.js +184 -184
- package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
- package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
- package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
- package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
- package/src/ruvocal/static/huggingchat/icon.svg +4 -4
- package/src/ruvocal/static/huggingchat/logo.svg +4 -4
- package/src/ruvocal/static/huggingchat/manifest.json +54 -54
- package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
- package/src/ruvocal/static/robots.txt +10 -10
- package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
- package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
- package/src/ruvocal/svelte.config.js +53 -53
- package/src/ruvocal/tailwind.config.cjs +30 -30
- package/src/ruvocal/tsconfig.json +19 -19
- package/src/ruvocal/vite.config.ts +87 -87
- package/src/scripts/deploy.sh +116 -116
- package/src/scripts/generate-config.js +245 -245
- package/src/scripts/generate-welcome.js +187 -187
- package/src/scripts/package-rvf.sh +116 -116
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* ADR-166 runtime verification — spawn each bridge with a test env and
|
|
4
|
-
* assert the load-bearing behaviors observed OVER THE WIRE. Complements
|
|
5
|
-
* test-security-lock.js (which is static-source only).
|
|
6
|
-
*
|
|
7
|
-
* Behaviors verified:
|
|
8
|
-
* R1. bridge starts on 127.0.0.1 by default
|
|
9
|
-
* R2. with MCP_AUTH_TOKEN set: unauthenticated POST /mcp → 401
|
|
10
|
-
* R3. with MCP_AUTH_TOKEN set: authenticated POST /mcp → 200 (or ≠401)
|
|
11
|
-
* R4. terminal_execute call → TOOL_DISABLED error unless MCP_ENABLE_TERMINAL=true
|
|
12
|
-
* R5. public bind without MCP_AUTH_TOKEN → process exits ≠0 within 3s
|
|
13
|
-
*
|
|
14
|
-
* node test-runtime-security.mjs
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { spawn } from "node:child_process";
|
|
18
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
|
|
22
|
-
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const BRIDGES = [
|
|
24
|
-
{ label: "src/mcp-bridge", entry: path.join(HERE, "index.js") },
|
|
25
|
-
{ label: "src/ruvocal/mcp-bridge", entry: path.join(HERE, "..", "ruvocal", "mcp-bridge", "index.js") },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const TOKEN = "test-token-" + Math.random().toString(36).slice(2);
|
|
29
|
-
let passed = 0, failed = 0;
|
|
30
|
-
const failures = [];
|
|
31
|
-
|
|
32
|
-
function assert(cond, label) {
|
|
33
|
-
if (cond) { console.log(` ✓ ${label}`); passed++; }
|
|
34
|
-
else { console.log(` ✗ ${label}`); failures.push(label); failed++; }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function pickPort() {
|
|
38
|
-
// Pick a random port in the ephemeral range and hope for the best;
|
|
39
|
-
// if the bridge fails to bind, the fetch will time out and we fail.
|
|
40
|
-
return 20000 + Math.floor(Math.random() * 10000);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function startBridge(entry, env, { waitMs = 1500 } = {}) {
|
|
44
|
-
const child = spawn("node", [entry], {
|
|
45
|
-
env: { ...process.env, ...env },
|
|
46
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
-
});
|
|
48
|
-
let stdout = "", stderr = "";
|
|
49
|
-
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
50
|
-
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
51
|
-
const exitPromise = new Promise((resolve) => {
|
|
52
|
-
child.on("exit", (code) => resolve({ exited: true, code }));
|
|
53
|
-
});
|
|
54
|
-
const ready = Promise.race([
|
|
55
|
-
exitPromise,
|
|
56
|
-
sleep(waitMs).then(() => ({ exited: false, code: null })),
|
|
57
|
-
]);
|
|
58
|
-
return { child, stdout: () => stdout, stderr: () => stderr, ready };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function killAndWait(child) {
|
|
62
|
-
if (child.exitCode !== null) return;
|
|
63
|
-
try { child.kill("SIGTERM"); } catch { /* ignore */ }
|
|
64
|
-
await sleep(100);
|
|
65
|
-
if (child.exitCode === null) try { child.kill("SIGKILL"); } catch { /* ignore */ }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function testBridge(label, entry) {
|
|
69
|
-
console.log(`\n# ${label}`);
|
|
70
|
-
|
|
71
|
-
// R1 + R2 + R3 + R4 — bridge starts with token, auth behavior + terminal gate
|
|
72
|
-
{
|
|
73
|
-
const port = await pickPort();
|
|
74
|
-
const b = await startBridge(entry, {
|
|
75
|
-
PORT: String(port),
|
|
76
|
-
MCP_BIND_HOST: "127.0.0.1",
|
|
77
|
-
MCP_AUTH_TOKEN: TOKEN,
|
|
78
|
-
MCP_ENABLE_TERMINAL: "false",
|
|
79
|
-
MCP_GROUP_DEVTOOLS: "false",
|
|
80
|
-
MCP_GROUP_INTELLIGENCE: "false",
|
|
81
|
-
MCP_GROUP_AGENTS: "false",
|
|
82
|
-
MCP_GROUP_MEMORY: "false",
|
|
83
|
-
});
|
|
84
|
-
const state = await b.ready;
|
|
85
|
-
if (state.exited) {
|
|
86
|
-
assert(false, `R1. bridge starts on 127.0.0.1 (exited early, code=${state.code}; stderr=${b.stderr().slice(0, 200)})`);
|
|
87
|
-
} else {
|
|
88
|
-
assert(true, "R1. bridge starts on 127.0.0.1 (didn't exit within 1.5s)");
|
|
89
|
-
|
|
90
|
-
// R2 — unauthenticated POST /mcp → 401
|
|
91
|
-
try {
|
|
92
|
-
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: { "Content-Type": "application/json" },
|
|
95
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
|
|
96
|
-
});
|
|
97
|
-
assert(r.status === 401, `R2. unauthenticated POST /mcp → 401 (got ${r.status})`);
|
|
98
|
-
} catch (e) {
|
|
99
|
-
assert(false, `R2. unauthenticated POST /mcp threw: ${e.message}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// R3 — authenticated POST /mcp → not 401
|
|
103
|
-
try {
|
|
104
|
-
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
105
|
-
method: "POST",
|
|
106
|
-
headers: {
|
|
107
|
-
"Content-Type": "application/json",
|
|
108
|
-
"Authorization": `Bearer ${TOKEN}`,
|
|
109
|
-
},
|
|
110
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
|
|
111
|
-
});
|
|
112
|
-
assert(r.status !== 401, `R3. authenticated POST /mcp → ≠401 (got ${r.status})`);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
assert(false, `R3. authenticated POST /mcp threw: ${e.message}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// R4 — terminal_execute → TOOL_DISABLED (denied at executeTool, not just autopilot)
|
|
118
|
-
try {
|
|
119
|
-
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
120
|
-
method: "POST",
|
|
121
|
-
headers: {
|
|
122
|
-
"Content-Type": "application/json",
|
|
123
|
-
"Authorization": `Bearer ${TOKEN}`,
|
|
124
|
-
},
|
|
125
|
-
body: JSON.stringify({
|
|
126
|
-
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
127
|
-
params: { name: "terminal_execute", arguments: { command: "id" } },
|
|
128
|
-
}),
|
|
129
|
-
});
|
|
130
|
-
const body = await r.json();
|
|
131
|
-
// The response wraps the executeTool result in `result.content[0].text` as JSON string.
|
|
132
|
-
const text = JSON.stringify(body);
|
|
133
|
-
assert(text.includes("TOOL_DISABLED") || text.includes("disabled by default"),
|
|
134
|
-
"R4. terminal_execute denied server-side (TOOL_DISABLED)");
|
|
135
|
-
} catch (e) {
|
|
136
|
-
assert(false, `R4. terminal_execute threw: ${e.message}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// R6 — #2425 djimit streamable-HTTP session cleanup: DELETE /mcp → 204
|
|
140
|
-
try {
|
|
141
|
-
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
142
|
-
method: "DELETE",
|
|
143
|
-
headers: {
|
|
144
|
-
"Authorization": `Bearer ${TOKEN}`,
|
|
145
|
-
"Mcp-Session-Id": "test-cleanup",
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
assert(r.status === 204, `R6. DELETE /mcp → 204 (got ${r.status})`);
|
|
149
|
-
assert(!!r.headers.get("Mcp-Session-Id"), "R6b. Mcp-Session-Id header echoed on /mcp*");
|
|
150
|
-
} catch (e) {
|
|
151
|
-
assert(false, `R6. DELETE /mcp threw: ${e.message}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// R7 — #2425 notifications/initialized returns 202 Accepted with empty body
|
|
155
|
-
try {
|
|
156
|
-
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
157
|
-
method: "POST",
|
|
158
|
-
headers: {
|
|
159
|
-
"Content-Type": "application/json",
|
|
160
|
-
"Authorization": `Bearer ${TOKEN}`,
|
|
161
|
-
},
|
|
162
|
-
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
163
|
-
});
|
|
164
|
-
assert(r.status === 202, `R7. notifications/initialized → 202 (got ${r.status})`);
|
|
165
|
-
assert((await r.text()) === "", "R7b. notifications/initialized has empty body");
|
|
166
|
-
} catch (e) {
|
|
167
|
-
assert(false, `R7. notifications/initialized threw: ${e.message}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
await killAndWait(b.child);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// R5 — public bind without token → process exits ≠0 within 3s
|
|
174
|
-
{
|
|
175
|
-
const port = await pickPort();
|
|
176
|
-
const b = await startBridge(entry, {
|
|
177
|
-
PORT: String(port),
|
|
178
|
-
MCP_BIND_HOST: "0.0.0.0",
|
|
179
|
-
MCP_AUTH_TOKEN: "",
|
|
180
|
-
MCP_GROUP_DEVTOOLS: "false",
|
|
181
|
-
MCP_GROUP_INTELLIGENCE: "false",
|
|
182
|
-
MCP_GROUP_AGENTS: "false",
|
|
183
|
-
MCP_GROUP_MEMORY: "false",
|
|
184
|
-
}, { waitMs: 3000 });
|
|
185
|
-
const state = await b.ready;
|
|
186
|
-
assert(state.exited && state.code !== 0,
|
|
187
|
-
`R5. public bind without token exits ≠0 (exited=${state.exited}, code=${state.code})`);
|
|
188
|
-
if (b.stderr().includes("FATAL")) {
|
|
189
|
-
assert(true, "R5b. FATAL message logged to stderr");
|
|
190
|
-
} else {
|
|
191
|
-
assert(false, "R5b. FATAL message expected in stderr");
|
|
192
|
-
}
|
|
193
|
-
await killAndWait(b.child);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
(async () => {
|
|
198
|
-
for (const { label, entry } of BRIDGES) {
|
|
199
|
-
await testBridge(label, entry);
|
|
200
|
-
}
|
|
201
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
202
|
-
if (failed > 0) {
|
|
203
|
-
console.log("\nFailures:");
|
|
204
|
-
for (const f of failures) console.log(` - ${f}`);
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
console.log("ADR-166 runtime verification: OK");
|
|
208
|
-
})();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ADR-166 runtime verification — spawn each bridge with a test env and
|
|
4
|
+
* assert the load-bearing behaviors observed OVER THE WIRE. Complements
|
|
5
|
+
* test-security-lock.js (which is static-source only).
|
|
6
|
+
*
|
|
7
|
+
* Behaviors verified:
|
|
8
|
+
* R1. bridge starts on 127.0.0.1 by default
|
|
9
|
+
* R2. with MCP_AUTH_TOKEN set: unauthenticated POST /mcp → 401
|
|
10
|
+
* R3. with MCP_AUTH_TOKEN set: authenticated POST /mcp → 200 (or ≠401)
|
|
11
|
+
* R4. terminal_execute call → TOOL_DISABLED error unless MCP_ENABLE_TERMINAL=true
|
|
12
|
+
* R5. public bind without MCP_AUTH_TOKEN → process exits ≠0 within 3s
|
|
13
|
+
*
|
|
14
|
+
* node test-runtime-security.mjs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const BRIDGES = [
|
|
24
|
+
{ label: "src/mcp-bridge", entry: path.join(HERE, "index.js") },
|
|
25
|
+
{ label: "src/ruvocal/mcp-bridge", entry: path.join(HERE, "..", "ruvocal", "mcp-bridge", "index.js") },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const TOKEN = "test-token-" + Math.random().toString(36).slice(2);
|
|
29
|
+
let passed = 0, failed = 0;
|
|
30
|
+
const failures = [];
|
|
31
|
+
|
|
32
|
+
function assert(cond, label) {
|
|
33
|
+
if (cond) { console.log(` ✓ ${label}`); passed++; }
|
|
34
|
+
else { console.log(` ✗ ${label}`); failures.push(label); failed++; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function pickPort() {
|
|
38
|
+
// Pick a random port in the ephemeral range and hope for the best;
|
|
39
|
+
// if the bridge fails to bind, the fetch will time out and we fail.
|
|
40
|
+
return 20000 + Math.floor(Math.random() * 10000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function startBridge(entry, env, { waitMs = 1500 } = {}) {
|
|
44
|
+
const child = spawn("node", [entry], {
|
|
45
|
+
env: { ...process.env, ...env },
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
let stdout = "", stderr = "";
|
|
49
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
50
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
51
|
+
const exitPromise = new Promise((resolve) => {
|
|
52
|
+
child.on("exit", (code) => resolve({ exited: true, code }));
|
|
53
|
+
});
|
|
54
|
+
const ready = Promise.race([
|
|
55
|
+
exitPromise,
|
|
56
|
+
sleep(waitMs).then(() => ({ exited: false, code: null })),
|
|
57
|
+
]);
|
|
58
|
+
return { child, stdout: () => stdout, stderr: () => stderr, ready };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function killAndWait(child) {
|
|
62
|
+
if (child.exitCode !== null) return;
|
|
63
|
+
try { child.kill("SIGTERM"); } catch { /* ignore */ }
|
|
64
|
+
await sleep(100);
|
|
65
|
+
if (child.exitCode === null) try { child.kill("SIGKILL"); } catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function testBridge(label, entry) {
|
|
69
|
+
console.log(`\n# ${label}`);
|
|
70
|
+
|
|
71
|
+
// R1 + R2 + R3 + R4 — bridge starts with token, auth behavior + terminal gate
|
|
72
|
+
{
|
|
73
|
+
const port = await pickPort();
|
|
74
|
+
const b = await startBridge(entry, {
|
|
75
|
+
PORT: String(port),
|
|
76
|
+
MCP_BIND_HOST: "127.0.0.1",
|
|
77
|
+
MCP_AUTH_TOKEN: TOKEN,
|
|
78
|
+
MCP_ENABLE_TERMINAL: "false",
|
|
79
|
+
MCP_GROUP_DEVTOOLS: "false",
|
|
80
|
+
MCP_GROUP_INTELLIGENCE: "false",
|
|
81
|
+
MCP_GROUP_AGENTS: "false",
|
|
82
|
+
MCP_GROUP_MEMORY: "false",
|
|
83
|
+
});
|
|
84
|
+
const state = await b.ready;
|
|
85
|
+
if (state.exited) {
|
|
86
|
+
assert(false, `R1. bridge starts on 127.0.0.1 (exited early, code=${state.code}; stderr=${b.stderr().slice(0, 200)})`);
|
|
87
|
+
} else {
|
|
88
|
+
assert(true, "R1. bridge starts on 127.0.0.1 (didn't exit within 1.5s)");
|
|
89
|
+
|
|
90
|
+
// R2 — unauthenticated POST /mcp → 401
|
|
91
|
+
try {
|
|
92
|
+
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
|
|
96
|
+
});
|
|
97
|
+
assert(r.status === 401, `R2. unauthenticated POST /mcp → 401 (got ${r.status})`);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
assert(false, `R2. unauthenticated POST /mcp threw: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// R3 — authenticated POST /mcp → not 401
|
|
103
|
+
try {
|
|
104
|
+
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"Authorization": `Bearer ${TOKEN}`,
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
|
|
111
|
+
});
|
|
112
|
+
assert(r.status !== 401, `R3. authenticated POST /mcp → ≠401 (got ${r.status})`);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
assert(false, `R3. authenticated POST /mcp threw: ${e.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// R4 — terminal_execute → TOOL_DISABLED (denied at executeTool, not just autopilot)
|
|
118
|
+
try {
|
|
119
|
+
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"Authorization": `Bearer ${TOKEN}`,
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
127
|
+
params: { name: "terminal_execute", arguments: { command: "id" } },
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
const body = await r.json();
|
|
131
|
+
// The response wraps the executeTool result in `result.content[0].text` as JSON string.
|
|
132
|
+
const text = JSON.stringify(body);
|
|
133
|
+
assert(text.includes("TOOL_DISABLED") || text.includes("disabled by default"),
|
|
134
|
+
"R4. terminal_execute denied server-side (TOOL_DISABLED)");
|
|
135
|
+
} catch (e) {
|
|
136
|
+
assert(false, `R4. terminal_execute threw: ${e.message}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// R6 — #2425 djimit streamable-HTTP session cleanup: DELETE /mcp → 204
|
|
140
|
+
try {
|
|
141
|
+
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
142
|
+
method: "DELETE",
|
|
143
|
+
headers: {
|
|
144
|
+
"Authorization": `Bearer ${TOKEN}`,
|
|
145
|
+
"Mcp-Session-Id": "test-cleanup",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
assert(r.status === 204, `R6. DELETE /mcp → 204 (got ${r.status})`);
|
|
149
|
+
assert(!!r.headers.get("Mcp-Session-Id"), "R6b. Mcp-Session-Id header echoed on /mcp*");
|
|
150
|
+
} catch (e) {
|
|
151
|
+
assert(false, `R6. DELETE /mcp threw: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// R7 — #2425 notifications/initialized returns 202 Accepted with empty body
|
|
155
|
+
try {
|
|
156
|
+
const r = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
"Authorization": `Bearer ${TOKEN}`,
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
163
|
+
});
|
|
164
|
+
assert(r.status === 202, `R7. notifications/initialized → 202 (got ${r.status})`);
|
|
165
|
+
assert((await r.text()) === "", "R7b. notifications/initialized has empty body");
|
|
166
|
+
} catch (e) {
|
|
167
|
+
assert(false, `R7. notifications/initialized threw: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await killAndWait(b.child);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// R5 — public bind without token → process exits ≠0 within 3s
|
|
174
|
+
{
|
|
175
|
+
const port = await pickPort();
|
|
176
|
+
const b = await startBridge(entry, {
|
|
177
|
+
PORT: String(port),
|
|
178
|
+
MCP_BIND_HOST: "0.0.0.0",
|
|
179
|
+
MCP_AUTH_TOKEN: "",
|
|
180
|
+
MCP_GROUP_DEVTOOLS: "false",
|
|
181
|
+
MCP_GROUP_INTELLIGENCE: "false",
|
|
182
|
+
MCP_GROUP_AGENTS: "false",
|
|
183
|
+
MCP_GROUP_MEMORY: "false",
|
|
184
|
+
}, { waitMs: 3000 });
|
|
185
|
+
const state = await b.ready;
|
|
186
|
+
assert(state.exited && state.code !== 0,
|
|
187
|
+
`R5. public bind without token exits ≠0 (exited=${state.exited}, code=${state.code})`);
|
|
188
|
+
if (b.stderr().includes("FATAL")) {
|
|
189
|
+
assert(true, "R5b. FATAL message logged to stderr");
|
|
190
|
+
} else {
|
|
191
|
+
assert(false, "R5b. FATAL message expected in stderr");
|
|
192
|
+
}
|
|
193
|
+
await killAndWait(b.child);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
(async () => {
|
|
198
|
+
for (const { label, entry } of BRIDGES) {
|
|
199
|
+
await testBridge(label, entry);
|
|
200
|
+
}
|
|
201
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
202
|
+
if (failed > 0) {
|
|
203
|
+
console.log("\nFailures:");
|
|
204
|
+
for (const f of failures) console.log(` - ${f}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
console.log("ADR-166 runtime verification: OK");
|
|
208
|
+
})();
|
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* ADR-166 §6 acceptance #9 — anti-regression lock for the MCP-bridge
|
|
4
|
-
* security surface. Runs on the source of BOTH bridge files (the ruvocal
|
|
5
|
-
* variant that ships in docker-compose, and the sibling REST bridge).
|
|
6
|
-
*
|
|
7
|
-
* Fails if any of the following load-bearing controls silently disappears:
|
|
8
|
-
* 1. `MCP_BIND_HOST` default is `127.0.0.1` (loopback default — Phase 1a)
|
|
9
|
-
* 2. Public bind without `MCP_AUTH_TOKEN` calls `process.exit(1)` at boot (Phase 1b)
|
|
10
|
-
* 3. `requireAuth` middleware exists and is mounted with `app.use(requireAuth)` (Phase 1c)
|
|
11
|
-
* 4. `requireAuth` uses `timingSafeEqual` (constant-time compare)
|
|
12
|
-
* 5. `executeTool` denies terminal_execute unless `MCP_ENABLE_TERMINAL === "true"` (Phase 1d + 2a)
|
|
13
|
-
* 6. CORS respects `MCP_CORS_ORIGIN` allowlist (Phase 3b)
|
|
14
|
-
* 9. Both production images use the supported Node 24 LTS line (#2847)
|
|
15
|
-
*
|
|
16
|
-
* Runs offline (no server, no deps). CI-safe.
|
|
17
|
-
*
|
|
18
|
-
* node test-security-lock.js # → exit 0 on green, exit 1 on regression
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import fs from "node:fs";
|
|
22
|
-
import path from "node:path";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
|
-
|
|
25
|
-
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const BRIDGES = [
|
|
27
|
-
path.join(HERE, "index.js"), // ruflo/src/mcp-bridge/index.js
|
|
28
|
-
path.join(HERE, "..", "ruvocal", "mcp-bridge", "index.js"), // ruvocal variant (deployed)
|
|
29
|
-
];
|
|
30
|
-
const DOCKERFILES = [
|
|
31
|
-
path.join(HERE, "Dockerfile"),
|
|
32
|
-
path.join(HERE, "..", "ruvocal", "mcp-bridge", "Dockerfile"),
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const CHECKS = [
|
|
36
|
-
{
|
|
37
|
-
id: "1. bind-loopback-default",
|
|
38
|
-
rx: /BIND_HOST\s*=\s*process\.env\.MCP_BIND_HOST\s*\|\|\s*["']127\.0\.0\.1["']/,
|
|
39
|
-
hint: "MCP_BIND_HOST must default to 127.0.0.1 (ADR-166 §6 Phase 1a)",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "2. fail-closed-public-bind",
|
|
43
|
-
rx: /isPublic\s*&&\s*!process\.env\.MCP_AUTH_TOKEN[\s\S]*?process\.exit\(\s*1\s*\)/,
|
|
44
|
-
hint: "public bind without MCP_AUTH_TOKEN must process.exit(1) at boot (Phase 1b)",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: "3. auth-middleware-mounted",
|
|
48
|
-
rx: /app\.use\(\s*requireAuth\s*\)/,
|
|
49
|
-
hint: "requireAuth middleware must be mounted via app.use() (Phase 1c)",
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "4. constant-time-token-compare",
|
|
53
|
-
rx: /timingSafeEqual\s*\(/,
|
|
54
|
-
hint: "auth compare must use timingSafeEqual (Phase 1c)",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: "5. terminal-gate-server-side",
|
|
58
|
-
rx: /MCP_ENABLE_TERMINAL[\s\S]*?executeTool[\s\S]*?TOOL_DISABLED|executeTool[\s\S]*?MCP_ENABLE_TERMINAL[\s\S]*?TOOL_DISABLED/,
|
|
59
|
-
hint: "terminal_execute must be denied at executeTool unless MCP_ENABLE_TERMINAL=true (Phase 1d + 2a)",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
id: "6. cors-allowlist-respected",
|
|
63
|
-
rx: /MCP_CORS_ORIGIN[\s\S]*?CORS_ALLOWLIST/,
|
|
64
|
-
hint: "CORS must respect MCP_CORS_ORIGIN allowlist (Phase 3b)",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: "7. streamable-http-delete-handler (#2425)",
|
|
68
|
-
rx: /app\.delete\(\s*["']\/mcp["']\s*,/,
|
|
69
|
-
hint: "DELETE /mcp must be handled for streamable-HTTP session cleanup (#2425)",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "8. mcp-session-id-header (#2425)",
|
|
73
|
-
rx: /Mcp-Session-Id/,
|
|
74
|
-
hint: "Mcp-Session-Id header must be echoed on /mcp* responses (#2425)",
|
|
75
|
-
},
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
let hardFail = false;
|
|
79
|
-
for (const bridge of BRIDGES) {
|
|
80
|
-
if (!fs.existsSync(bridge)) {
|
|
81
|
-
console.error(`FAIL: bridge missing: ${bridge}`);
|
|
82
|
-
hardFail = true;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const src = fs.readFileSync(bridge, "utf-8");
|
|
86
|
-
const rel = path.relative(path.join(HERE, "..", ".."), bridge);
|
|
87
|
-
console.log(`\n# ${rel}`);
|
|
88
|
-
for (const check of CHECKS) {
|
|
89
|
-
if (check.rx.test(src)) {
|
|
90
|
-
console.log(` ✓ ${check.id}`);
|
|
91
|
-
} else {
|
|
92
|
-
console.log(` ✗ ${check.id}`);
|
|
93
|
-
console.log(` hint: ${check.hint}`);
|
|
94
|
-
hardFail = true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (const dockerfile of DOCKERFILES) {
|
|
100
|
-
if (!fs.existsSync(dockerfile)) {
|
|
101
|
-
console.error(`FAIL: Dockerfile missing: ${dockerfile}`);
|
|
102
|
-
hardFail = true;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const source = fs.readFileSync(dockerfile, "utf-8");
|
|
106
|
-
const rel = path.relative(path.join(HERE, "..", ".."), dockerfile);
|
|
107
|
-
console.log(`\n# ${rel}`);
|
|
108
|
-
if (/^FROM\s+node:24(?:[.-][^\s]+)?\s*$/m.test(source)) {
|
|
109
|
-
console.log(" ✓ 9. supported-node-lts-runtime (#2847)");
|
|
110
|
-
} else {
|
|
111
|
-
console.log(" ✗ 9. supported-node-lts-runtime (#2847)");
|
|
112
|
-
console.log(" hint: production images must use the supported node:24 LTS line");
|
|
113
|
-
hardFail = true;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (hardFail) {
|
|
118
|
-
console.error("\nADR-166 security lock FAILED — a load-bearing control was removed or drifted.");
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
console.log("\nADR-166 security lock: OK");
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ADR-166 §6 acceptance #9 — anti-regression lock for the MCP-bridge
|
|
4
|
+
* security surface. Runs on the source of BOTH bridge files (the ruvocal
|
|
5
|
+
* variant that ships in docker-compose, and the sibling REST bridge).
|
|
6
|
+
*
|
|
7
|
+
* Fails if any of the following load-bearing controls silently disappears:
|
|
8
|
+
* 1. `MCP_BIND_HOST` default is `127.0.0.1` (loopback default — Phase 1a)
|
|
9
|
+
* 2. Public bind without `MCP_AUTH_TOKEN` calls `process.exit(1)` at boot (Phase 1b)
|
|
10
|
+
* 3. `requireAuth` middleware exists and is mounted with `app.use(requireAuth)` (Phase 1c)
|
|
11
|
+
* 4. `requireAuth` uses `timingSafeEqual` (constant-time compare)
|
|
12
|
+
* 5. `executeTool` denies terminal_execute unless `MCP_ENABLE_TERMINAL === "true"` (Phase 1d + 2a)
|
|
13
|
+
* 6. CORS respects `MCP_CORS_ORIGIN` allowlist (Phase 3b)
|
|
14
|
+
* 9. Both production images use the supported Node 24 LTS line (#2847)
|
|
15
|
+
*
|
|
16
|
+
* Runs offline (no server, no deps). CI-safe.
|
|
17
|
+
*
|
|
18
|
+
* node test-security-lock.js # → exit 0 on green, exit 1 on regression
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const BRIDGES = [
|
|
27
|
+
path.join(HERE, "index.js"), // ruflo/src/mcp-bridge/index.js
|
|
28
|
+
path.join(HERE, "..", "ruvocal", "mcp-bridge", "index.js"), // ruvocal variant (deployed)
|
|
29
|
+
];
|
|
30
|
+
const DOCKERFILES = [
|
|
31
|
+
path.join(HERE, "Dockerfile"),
|
|
32
|
+
path.join(HERE, "..", "ruvocal", "mcp-bridge", "Dockerfile"),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const CHECKS = [
|
|
36
|
+
{
|
|
37
|
+
id: "1. bind-loopback-default",
|
|
38
|
+
rx: /BIND_HOST\s*=\s*process\.env\.MCP_BIND_HOST\s*\|\|\s*["']127\.0\.0\.1["']/,
|
|
39
|
+
hint: "MCP_BIND_HOST must default to 127.0.0.1 (ADR-166 §6 Phase 1a)",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "2. fail-closed-public-bind",
|
|
43
|
+
rx: /isPublic\s*&&\s*!process\.env\.MCP_AUTH_TOKEN[\s\S]*?process\.exit\(\s*1\s*\)/,
|
|
44
|
+
hint: "public bind without MCP_AUTH_TOKEN must process.exit(1) at boot (Phase 1b)",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "3. auth-middleware-mounted",
|
|
48
|
+
rx: /app\.use\(\s*requireAuth\s*\)/,
|
|
49
|
+
hint: "requireAuth middleware must be mounted via app.use() (Phase 1c)",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "4. constant-time-token-compare",
|
|
53
|
+
rx: /timingSafeEqual\s*\(/,
|
|
54
|
+
hint: "auth compare must use timingSafeEqual (Phase 1c)",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "5. terminal-gate-server-side",
|
|
58
|
+
rx: /MCP_ENABLE_TERMINAL[\s\S]*?executeTool[\s\S]*?TOOL_DISABLED|executeTool[\s\S]*?MCP_ENABLE_TERMINAL[\s\S]*?TOOL_DISABLED/,
|
|
59
|
+
hint: "terminal_execute must be denied at executeTool unless MCP_ENABLE_TERMINAL=true (Phase 1d + 2a)",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "6. cors-allowlist-respected",
|
|
63
|
+
rx: /MCP_CORS_ORIGIN[\s\S]*?CORS_ALLOWLIST/,
|
|
64
|
+
hint: "CORS must respect MCP_CORS_ORIGIN allowlist (Phase 3b)",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "7. streamable-http-delete-handler (#2425)",
|
|
68
|
+
rx: /app\.delete\(\s*["']\/mcp["']\s*,/,
|
|
69
|
+
hint: "DELETE /mcp must be handled for streamable-HTTP session cleanup (#2425)",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "8. mcp-session-id-header (#2425)",
|
|
73
|
+
rx: /Mcp-Session-Id/,
|
|
74
|
+
hint: "Mcp-Session-Id header must be echoed on /mcp* responses (#2425)",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
let hardFail = false;
|
|
79
|
+
for (const bridge of BRIDGES) {
|
|
80
|
+
if (!fs.existsSync(bridge)) {
|
|
81
|
+
console.error(`FAIL: bridge missing: ${bridge}`);
|
|
82
|
+
hardFail = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const src = fs.readFileSync(bridge, "utf-8");
|
|
86
|
+
const rel = path.relative(path.join(HERE, "..", ".."), bridge);
|
|
87
|
+
console.log(`\n# ${rel}`);
|
|
88
|
+
for (const check of CHECKS) {
|
|
89
|
+
if (check.rx.test(src)) {
|
|
90
|
+
console.log(` ✓ ${check.id}`);
|
|
91
|
+
} else {
|
|
92
|
+
console.log(` ✗ ${check.id}`);
|
|
93
|
+
console.log(` hint: ${check.hint}`);
|
|
94
|
+
hardFail = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const dockerfile of DOCKERFILES) {
|
|
100
|
+
if (!fs.existsSync(dockerfile)) {
|
|
101
|
+
console.error(`FAIL: Dockerfile missing: ${dockerfile}`);
|
|
102
|
+
hardFail = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const source = fs.readFileSync(dockerfile, "utf-8");
|
|
106
|
+
const rel = path.relative(path.join(HERE, "..", ".."), dockerfile);
|
|
107
|
+
console.log(`\n# ${rel}`);
|
|
108
|
+
if (/^FROM\s+node:24(?:[.-][^\s]+)?\s*$/m.test(source)) {
|
|
109
|
+
console.log(" ✓ 9. supported-node-lts-runtime (#2847)");
|
|
110
|
+
} else {
|
|
111
|
+
console.log(" ✗ 9. supported-node-lts-runtime (#2847)");
|
|
112
|
+
console.log(" hint: production images must use the supported node:24 LTS line");
|
|
113
|
+
hardFail = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hardFail) {
|
|
118
|
+
console.error("\nADR-166 security lock FAILED — a load-bearing control was removed or drifted.");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
console.log("\nADR-166 security lock: OK");
|