diff --git a/.secrets.baseline b/.secrets.baseline index 826d5b4de..57a0557c7 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -146,6 +146,22 @@ } ], "results": { + ".detect-secrets.cfg": [ + { + "type": "Private Key", + "filename": ".detect-secrets.cfg", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", + "is_verified": false, + "line_number": 17 + }, + { + "type": "Secret Keyword", + "filename": ".detect-secrets.cfg", + "hashed_secret": "fe88fceb47e040ba1bfafa4ac639366188df2f6d", + "is_verified": false, + "line_number": 19 + } + ], ".env.example": [ { "type": "Twilio API Key", @@ -156,75 +172,75 @@ } ], "appcast.xml": [ + { + "type": "Base64 High Entropy String", + "filename": "appcast.xml", + "hashed_secret": "c417f47cebc049c2cfb215b454ac87fee4097e5d", + "is_verified": false, + "line_number": 19 + }, + { + "type": "Base64 High Entropy String", + "filename": "appcast.xml", + "hashed_secret": "70632487bab4b6634ef5bd60d0979b169cf7440a", + "is_verified": false, + "line_number": 98 + }, { "type": "Base64 High Entropy String", "filename": "appcast.xml", "hashed_secret": "4e5f0a148d9ef42afeb73b1c77643e2ef2dee0b9", "is_verified": false, - "line_number": 90 - }, - { - "type": "Base64 High Entropy String", - "filename": "appcast.xml", - "hashed_secret": "f1ccdaf78c308ec2cf608818da13f5f1e4809ed1", - "is_verified": false, - "line_number": 138 - }, - { - "type": "Base64 High Entropy String", - "filename": "appcast.xml", - "hashed_secret": "2691dc9c9ded92ba62a2d8ee589e2d78e2aa0479", - "is_verified": false, - "line_number": 212 + "line_number": 185 } ], - "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift": [ + "apps/macos/Tests/MoltbotIPCTests/AnthropicAuthResolverTests.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift", + "filename": "apps/macos/Tests/MoltbotIPCTests/AnthropicAuthResolverTests.swift", "hashed_secret": "e761624445731fcb8b15da94343c6b92e507d190", "is_verified": false, "line_number": 26 }, { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift", + "filename": "apps/macos/Tests/MoltbotIPCTests/AnthropicAuthResolverTests.swift", "hashed_secret": "a23c8630c8a5fbaa21f095e0269c135c20d21689", "is_verified": false, "line_number": 42 } ], - "apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift": [ + "apps/macos/Tests/MoltbotIPCTests/GatewayEndpointStoreTests.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift", + "filename": "apps/macos/Tests/MoltbotIPCTests/GatewayEndpointStoreTests.swift", "hashed_secret": "19dad5cecb110281417d1db56b60e1b006d55bb4", "is_verified": false, "line_number": 61 } ], - "apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift": [ + "apps/macos/Tests/MoltbotIPCTests/GatewayLaunchAgentManagerTests.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift", + "filename": "apps/macos/Tests/MoltbotIPCTests/GatewayLaunchAgentManagerTests.swift", "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_verified": false, "line_number": 13 } ], - "apps/macos/Tests/ClawdbotIPCTests/TailscaleIntegrationSectionTests.swift": [ + "apps/macos/Tests/MoltbotIPCTests/TailscaleIntegrationSectionTests.swift": [ { "type": "Secret Keyword", - "filename": "apps/macos/Tests/ClawdbotIPCTests/TailscaleIntegrationSectionTests.swift", + "filename": "apps/macos/Tests/MoltbotIPCTests/TailscaleIntegrationSectionTests.swift", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, "line_number": 27 } ], - "apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift": [ + "apps/shared/MoltbotKit/Sources/MoltbotKit/GatewayChannel.swift": [ { "type": "Secret Keyword", - "filename": "apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift", + "filename": "apps/shared/MoltbotKit/Sources/MoltbotKit/GatewayChannel.swift", "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "is_verified": false, "line_number": 100 @@ -248,6 +264,15 @@ "line_number": 32 } ], + "docs/channels/line.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/line.md", + "hashed_secret": "83661b43df128631f891767fbfc5b049af3dce86", + "is_verified": false, + "line_number": 60 + } + ], "docs/channels/matrix.md": [ { "type": "Secret Keyword", @@ -284,27 +309,43 @@ "line_number": 141 } ], + "docs/channels/twitch.md": [ + { + "type": "Secret Keyword", + "filename": "docs/channels/twitch.md", + "hashed_secret": "0d1ba0da3e84e54f29846c93c43182eede365858", + "is_verified": false, + "line_number": 132 + }, + { + "type": "Secret Keyword", + "filename": "docs/channels/twitch.md", + "hashed_secret": "7cb4c5b8b81e266d08d4f106799af98d748bceb9", + "is_verified": false, + "line_number": 312 + } + ], "docs/concepts/memory.md": [ { "type": "Secret Keyword", "filename": "docs/concepts/memory.md", "hashed_secret": "39d711243bfcee9fec8299b204e1aa9c3430fa12", "is_verified": false, - "line_number": 108 + "line_number": 132 }, { "type": "Secret Keyword", "filename": "docs/concepts/memory.md", "hashed_secret": "1a8abbf465c52363ab4c9c6ad945b8e857cbea55", "is_verified": false, - "line_number": 131 + "line_number": 155 }, { "type": "Secret Keyword", "filename": "docs/concepts/memory.md", "hashed_secret": "b9f640d6095b9f6b5a65983f7b76dbbb254e0044", "is_verified": false, - "line_number": 373 + "line_number": 397 } ], "docs/concepts/model-providers.md": [ @@ -313,14 +354,14 @@ "filename": "docs/concepts/model-providers.md", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_verified": false, - "line_number": 168 + "line_number": 171 }, { "type": "Secret Keyword", "filename": "docs/concepts/model-providers.md", "hashed_secret": "ef83ad68b9b66e008727b7c417c6a8f618b5177e", "is_verified": false, - "line_number": 255 + "line_number": 282 } ], "docs/environment.md": [ @@ -359,21 +400,21 @@ "filename": "docs/gateway/configuration-examples.md", "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", "is_verified": false, - "line_number": 319 + "line_number": 320 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-examples.md", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "is_verified": false, - "line_number": 414 + "line_number": 415 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-examples.md", "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", "is_verified": false, - "line_number": 548 + "line_number": 549 } ], "docs/gateway/configuration.md": [ @@ -382,63 +423,63 @@ "filename": "docs/gateway/configuration.md", "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", "is_verified": false, - "line_number": 272 + "line_number": 283 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", "is_verified": false, - "line_number": 274 + "line_number": 285 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 1029 + "line_number": 1037 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", "is_verified": false, - "line_number": 1470 + "line_number": 1519 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", "is_verified": false, - "line_number": 1486 + "line_number": 1535 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", "is_verified": false, - "line_number": 2268 + "line_number": 2320 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_verified": false, - "line_number": 2344 + "line_number": 2396 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "is_verified": false, - "line_number": 2658 + "line_number": 2711 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration.md", "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", "is_verified": false, - "line_number": 2844 + "line_number": 2907 } ], "docs/gateway/local-models.md": [ @@ -463,7 +504,7 @@ "filename": "docs/gateway/tailscale.md", "hashed_secret": "9cb0dc5383312aa15b9dc6745645bde18ff5ade9", "is_verified": false, - "line_number": 75 + "line_number": 78 } ], "docs/help/faq.md": [ @@ -472,35 +513,35 @@ "filename": "docs/help/faq.md", "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", "is_verified": false, - "line_number": 925 + "line_number": 1333 }, { "type": "Secret Keyword", "filename": "docs/help/faq.md", "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", "is_verified": false, - "line_number": 1113 + "line_number": 1595 }, { "type": "Secret Keyword", "filename": "docs/help/faq.md", "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", "is_verified": false, - "line_number": 1114 + "line_number": 1596 }, { "type": "Secret Keyword", "filename": "docs/help/faq.md", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_verified": false, - "line_number": 1439 + "line_number": 2007 }, { "type": "Secret Keyword", "filename": "docs/help/faq.md", "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", "is_verified": false, - "line_number": 1715 + "line_number": 2279 } ], "docs/nodes/talk.md": [ @@ -521,13 +562,40 @@ "line_number": 35 } ], + "docs/platforms/macos-vm.md": [ + { + "type": "Secret Keyword", + "filename": "docs/platforms/macos-vm.md", + "hashed_secret": "8dd3bcd07c9ee927e6921c98b4dc6e94e2cc10a9", + "is_verified": false, + "line_number": 212 + } + ], + "docs/plugins/voice-call.md": [ + { + "type": "Secret Keyword", + "filename": "docs/plugins/voice-call.md", + "hashed_secret": "cb46980ce5532f18440dff4bbbe097896a8c08c8", + "is_verified": false, + "line_number": 158 + } + ], "docs/providers/anthropic.md": [ { "type": "Secret Keyword", "filename": "docs/providers/anthropic.md", "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", "is_verified": false, - "line_number": 32 + "line_number": 31 + } + ], + "docs/providers/claude-max-api-proxy.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/claude-max-api-proxy.md", + "hashed_secret": "b5c2827eb65bf13b87130e7e3c424ba9ff07cd67", + "is_verified": false, + "line_number": 77 } ], "docs/providers/glm.md": [ @@ -561,7 +629,16 @@ "filename": "docs/providers/moonshot.md", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_verified": false, - "line_number": 39 + "line_number": 40 + } + ], + "docs/providers/ollama.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/ollama.md", + "hashed_secret": "e774aaeac31c6272107ba89080295e277050fa7c", + "is_verified": false, + "line_number": 29 } ], "docs/providers/openai.md": [ @@ -570,7 +647,7 @@ "filename": "docs/providers/openai.md", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_verified": false, - "line_number": 31 + "line_number": 29 } ], "docs/providers/opencode.md": [ @@ -600,6 +677,22 @@ "line_number": 31 } ], + "docs/providers/venice.md": [ + { + "type": "Secret Keyword", + "filename": "docs/providers/venice.md", + "hashed_secret": "0b1b9301d9cd541620de4e3865d4a8f54f42fa89", + "is_verified": false, + "line_number": 53 + }, + { + "type": "Secret Keyword", + "filename": "docs/providers/venice.md", + "hashed_secret": "c179fe46776696372a90218532dc0d67267f2f04", + "is_verified": false, + "line_number": 233 + } + ], "docs/providers/zai.md": [ { "type": "Secret Keyword", @@ -615,7 +708,7 @@ "filename": "docs/tools/browser.md", "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_verified": false, - "line_number": 163 + "line_number": 136 } ], "docs/tools/firecrawl.md": [ @@ -642,7 +735,7 @@ "filename": "docs/tools/skills.md", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "is_verified": false, - "line_number": 160 + "line_number": 168 } ], "docs/tools/web.md": [ @@ -672,7 +765,7 @@ "filename": "docs/tools/web.md", "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", "is_verified": false, - "line_number": 223 + "line_number": 230 } ], "docs/tts.md": [ @@ -681,14 +774,14 @@ "filename": "docs/tts.md", "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", "is_verified": false, - "line_number": 72 + "line_number": 93 }, { "type": "Secret Keyword", "filename": "docs/tts.md", "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", "is_verified": false, - "line_number": 77 + "line_number": 98 } ], "extensions/bluebubbles/src/actions.test.ts": [ @@ -780,14 +873,14 @@ "filename": "extensions/bluebubbles/src/monitor.test.ts", "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "is_verified": false, - "line_number": 187 + "line_number": 193 }, { "type": "Secret Keyword", "filename": "extensions/bluebubbles/src/monitor.test.ts", "hashed_secret": "1ae0af3fe72b3ba394f9fa95a6cffc090d726c23", "is_verified": false, - "line_number": 394 + "line_number": 400 } ], "extensions/bluebubbles/src/reactions.test.ts": [ @@ -833,7 +926,7 @@ "filename": "extensions/bluebubbles/src/send.test.ts", "hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d", "is_verified": false, - "line_number": 675 + "line_number": 767 } ], "extensions/bluebubbles/src/targets.test.ts": [ @@ -872,6 +965,15 @@ "line_number": 9 } ], + "extensions/google-gemini-cli-auth/oauth.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/google-gemini-cli-auth/oauth.test.ts", + "hashed_secret": "021343c1f561d7bcbc3b513df45cc3a6baf67b43", + "is_verified": false, + "line_number": 26 + } + ], "extensions/matrix/src/matrix/accounts.test.ts": [ { "type": "Secret Keyword", @@ -955,7 +1057,7 @@ "filename": "extensions/nextcloud-talk/src/channel.ts", "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "is_verified": false, - "line_number": 390 + "line_number": 391 } ], "extensions/nostr/README.md": [ @@ -1109,6 +1211,40 @@ "line_number": 198 } ], + "extensions/twitch/src/onboarding.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/twitch/src/onboarding.test.ts", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", + "is_verified": false, + "line_number": 234 + }, + { + "type": "Secret Keyword", + "filename": "extensions/twitch/src/onboarding.test.ts", + "hashed_secret": "c8d8f8140951794fa875ea2c2d010c4382f36566", + "is_verified": false, + "line_number": 244 + } + ], + "extensions/twitch/src/status.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/twitch/src/status.test.ts", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", + "is_verified": false, + "line_number": 122 + } + ], + "extensions/voice-call/src/config.test.ts": [ + { + "type": "Secret Keyword", + "filename": "extensions/voice-call/src/config.test.ts", + "hashed_secret": "62207a469ec2fdcfc7d66b04c2980ac1501acbf0", + "is_verified": false, + "line_number": 122 + } + ], "extensions/zalo/README.md": [ { "type": "Secret Keyword", @@ -1169,7 +1305,7 @@ "filename": "src/agents/memory-search.test.ts", "hashed_secret": "a1b49d68a91fdf9c9217773f3fac988d77fa0f50", "is_verified": false, - "line_number": 164 + "line_number": 187 } ], "src/agents/model-auth.test.ts": [ @@ -1281,14 +1417,14 @@ "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", "is_verified": false, - "line_number": 112 + "line_number": 120 }, { "type": "Secret Keyword", "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", "hashed_secret": "94c4be5a1976115e8152960c21e04400a4fccdf6", "is_verified": false, - "line_number": 146 + "line_number": 154 } ], "src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts": [ @@ -1387,14 +1523,23 @@ "filename": "src/agents/pi-embedded-runner.test.ts", "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "is_verified": false, - "line_number": 117 + "line_number": 118 }, { "type": "Secret Keyword", "filename": "src/agents/pi-embedded-runner.test.ts", "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", "is_verified": false, - "line_number": 178 + "line_number": 179 + } + ], + "src/agents/pi-embedded-runner/run.overflow-compaction.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/agents/pi-embedded-runner/run.overflow-compaction.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "is_verified": false, + "line_number": 30 } ], "src/agents/skills.applyskillenvoverrides.test.ts": [ @@ -1446,28 +1591,28 @@ "filename": "src/agents/tools/web-search.ts", "hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b", "is_verified": false, - "line_number": 85 + "line_number": 93 }, { "type": "Secret Keyword", "filename": "src/agents/tools/web-search.ts", "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "is_verified": false, - "line_number": 190 + "line_number": 198 }, { "type": "Secret Keyword", "filename": "src/agents/tools/web-search.ts", "hashed_secret": "c4865ff9250aca23b0d98eb079dad70ebec1cced", "is_verified": false, - "line_number": 198 + "line_number": 206 }, { "type": "Secret Keyword", "filename": "src/agents/tools/web-search.ts", "hashed_secret": "527ee41f36386e85fa932ef09471ca017f3c95c8", "is_verified": false, - "line_number": 199 + "line_number": 207 } ], "src/agents/tools/web-tools.enabled-defaults.test.ts": [ @@ -1476,14 +1621,14 @@ "filename": "src/agents/tools/web-tools.enabled-defaults.test.ts", "hashed_secret": "47b249a75ca78fdb578d0f28c33685e27ea82684", "is_verified": false, - "line_number": 213 + "line_number": 268 }, { "type": "Secret Keyword", "filename": "src/agents/tools/web-tools.enabled-defaults.test.ts", "hashed_secret": "d0ffd81d6d7ad1bc3c365660fe8882480c9a986e", "is_verified": false, - "line_number": 242 + "line_number": 297 } ], "src/agents/tools/web-tools.fetch.test.ts": [ @@ -1533,7 +1678,7 @@ "filename": "src/auto-reply/status.test.ts", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_verified": false, - "line_number": 20 + "line_number": 33 } ], "src/browser/cdp.helpers.test.ts": [ @@ -1563,6 +1708,15 @@ "line_number": 13 } ], + "src/channels/plugins/normalize/signal.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "src/channels/plugins/normalize/signal.test.ts", + "hashed_secret": "99c962e8c62296bdc9a17f5caf91ce9bb4c7e0e6", + "is_verified": false, + "line_number": 24 + } + ], "src/cli/update-cli.test.ts": [ { "type": "Hex High Entropy String", @@ -1610,7 +1764,16 @@ "filename": "src/commands/configure.gateway-auth.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 8 + "line_number": 10 + } + ], + "src/commands/daemon-install-helpers.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/commands/daemon-install-helpers.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "is_verified": false, + "line_number": 122 } ], "src/commands/models/list.status.test.ts": [ @@ -1771,41 +1934,50 @@ "line_number": 228 } ], + "src/config/model-alias-defaults.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/config/model-alias-defaults.test.ts", + "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", + "is_verified": false, + "line_number": 66 + } + ], "src/config/schema.ts": [ { "type": "Secret Keyword", "filename": "src/config/schema.ts", "hashed_secret": "e73c9fcad85cd4eecc74181ec4bdb31064d68439", "is_verified": false, - "line_number": 184 + "line_number": 192 }, { "type": "Secret Keyword", "filename": "src/config/schema.ts", "hashed_secret": "2eda7cd978f39eebec3bf03e4410a40e14167fff", "is_verified": false, - "line_number": 220 + "line_number": 230 }, { "type": "Secret Keyword", "filename": "src/config/schema.ts", "hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208", "is_verified": false, - "line_number": 418 + "line_number": 439 }, { "type": "Secret Keyword", "filename": "src/config/schema.ts", "hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae", "is_verified": false, - "line_number": 437 + "line_number": 458 }, { "type": "Secret Keyword", "filename": "src/config/schema.ts", "hashed_secret": "bb7dfd9746e660e4a4374951ec5938ef0e343255", "is_verified": false, - "line_number": 487 + "line_number": 510 } ], "src/config/slack-http-config.test.ts": [ @@ -1905,14 +2077,14 @@ "filename": "src/gateway/server.auth.e2e.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 179 + "line_number": 192 }, { "type": "Secret Keyword", "filename": "src/gateway/server.auth.e2e.test.ts", "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "is_verified": false, - "line_number": 197 + "line_number": 210 } ], "src/gateway/session-utils.test.ts": [ @@ -1930,7 +2102,7 @@ "filename": "src/gateway/tools-invoke-http.test.ts", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 56 + "line_number": 133 } ], "src/gateway/ws-log.test.ts": [ @@ -1971,7 +2143,7 @@ "filename": "src/infra/outbound/message-action-runner.test.ts", "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "is_verified": false, - "line_number": 385 + "line_number": 423 } ], "src/infra/outbound/outbound-policy.test.ts": [ @@ -2006,6 +2178,79 @@ "line_number": 61 } ], + "src/line/accounts.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/accounts.test.ts", + "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", + "is_verified": false, + "line_number": 31 + }, + { + "type": "Secret Keyword", + "filename": "src/line/accounts.test.ts", + "hashed_secret": "8a8281cec699f5e51330e21dd7fab3531af6ef0c", + "is_verified": false, + "line_number": 49 + }, + { + "type": "Secret Keyword", + "filename": "src/line/accounts.test.ts", + "hashed_secret": "b4924d9834a1126714643ac231fb6623c14c3449", + "is_verified": false, + "line_number": 75 + } + ], + "src/line/bot-handlers.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/bot-handlers.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 63 + } + ], + "src/line/bot-message-context.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/bot-message-context.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 18 + } + ], + "src/line/signature.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/signature.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 10 + } + ], + "src/line/webhook.test.ts": [ + { + "type": "Secret Keyword", + "filename": "src/line/webhook.test.ts", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 22 + }, + { + "type": "Secret Keyword", + "filename": "src/line/webhook.test.ts", + "hashed_secret": "e2b11b2ed070b77e4b45630e0e59171ae5b2b674", + "is_verified": false, + "line_number": 94 + }, + { + "type": "Secret Keyword", + "filename": "src/line/webhook.test.ts", + "hashed_secret": "7deeb1b520a95d17eb2f4cf59b05d5c5f9e71e4d", + "is_verified": false, + "line_number": 95 + } + ], "src/logging/redact.test.ts": [ { "type": "Base64 High Entropy String", @@ -2096,21 +2341,21 @@ "filename": "src/memory/embeddings.test.ts", "hashed_secret": "a47110e348a3063541fb1f1f640d635d457181a0", "is_verified": false, - "line_number": 32 + "line_number": 34 }, { "type": "Secret Keyword", "filename": "src/memory/embeddings.test.ts", "hashed_secret": "c734e47630dda71619c696d88381f06f7511bd78", "is_verified": false, - "line_number": 149 + "line_number": 151 }, { "type": "Secret Keyword", "filename": "src/memory/embeddings.test.ts", "hashed_secret": "56e1d57b8db262b08bc73c60ed08d8c92e59503f", "is_verified": false, - "line_number": 179 + "line_number": 181 } ], "src/pairing/pairing-store.ts": [ @@ -2123,33 +2368,19 @@ } ], "src/security/audit.test.ts": [ - { - "type": "Hex High Entropy String", - "filename": "src/security/audit.test.ts", - "hashed_secret": "b1775a785f09a6ebaf2dc33d6eaeb98974d9cdb8", - "is_verified": false, - "line_number": 180 - }, - { - "type": "Hex High Entropy String", - "filename": "src/security/audit.test.ts", - "hashed_secret": "fa8da98a5bdb77b4902cbb4338e6e94ea825300e", - "is_verified": false, - "line_number": 209 - }, { "type": "Secret Keyword", "filename": "src/security/audit.test.ts", "hashed_secret": "21f688ab56f76a99e5c6ed342291422f4e57e47f", "is_verified": false, - "line_number": 1046 + "line_number": 1187 }, { "type": "Secret Keyword", "filename": "src/security/audit.test.ts", "hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e", "is_verified": false, - "line_number": 1077 + "line_number": 1218 } ], "src/tts/tts.test.ts": [ @@ -2165,7 +2396,28 @@ "filename": "src/tts/tts.test.ts", "hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb", "is_verified": false, - "line_number": 68 + "line_number": 71 + }, + { + "type": "Secret Keyword", + "filename": "src/tts/tts.test.ts", + "hashed_secret": "75ddfb45216fe09680dfe70eda4f559a910c832c", + "is_verified": false, + "line_number": 396 + }, + { + "type": "Secret Keyword", + "filename": "src/tts/tts.test.ts", + "hashed_secret": "e29af93630aa18cc3457cb5b13937b7ab7c99c9b", + "is_verified": false, + "line_number": 412 + }, + { + "type": "Secret Keyword", + "filename": "src/tts/tts.test.ts", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", + "is_verified": false, + "line_number": 446 } ], "src/web/qr-image.test.ts": [ @@ -2185,7 +2437,16 @@ "is_verified": false, "line_number": 182 } + ], + "vendor/a2ui/README.md": [ + { + "type": "Secret Keyword", + "filename": "vendor/a2ui/README.md", + "hashed_secret": "2619a5397a5d054dab3fe24e6a8da1fbd76ec3a6", + "is_verified": false, + "line_number": 123 + } ] }, - "generated_at": "2026-01-25T10:55:04Z" + "generated_at": "2026-01-29T10:03:38Z" } diff --git a/apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt b/apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt index 5fd429e9e..f92cfd293 100644 --- a/apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/bot/molt/android/NodeRuntime.kt @@ -3,6 +3,7 @@ package bot.molt.android import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.net.Uri import android.location.LocationManager import android.os.Build import android.os.SystemClock @@ -257,6 +258,36 @@ class NodeRuntime(context: Context) { return if (trimmed.isEmpty()) "main" else trimmed } + private fun isTrustedCanvasA2uiSource(): Boolean { + val rawUrl = canvas.currentUrl()?.trim().orEmpty() + if (rawUrl.isEmpty()) return false + + val uri = + try { + Uri.parse(rawUrl) + } catch (_: Throwable) { + return false + } + val scheme = uri.scheme?.trim()?.lowercase().orEmpty() + if (scheme != "http" && scheme != "https") return false + + val host = uri.host?.trim()?.lowercase().orEmpty() + if (host.isEmpty()) return false + + // Only accept A2UI actions from the currently-connected gateway origin(s). + val endpoint = connectedEndpoint + val allowedHosts = + listOfNotNull(endpoint?.host, endpoint?.lanHost, endpoint?.tailnetDns) + .map { it.trim().lowercase() } + .filter { it.isNotEmpty() } + .toSet() + + if (host in allowedHosts) return true + // Dev/loopback convenience (should not be reachable on public networks). + if (host == "localhost" || host == "127.0.0.1") return true + return false + } + private fun maybeNavigateToA2uiOnConnect() { val a2uiUrl = resolveA2uiHostUrl() ?: return val current = canvas.currentUrl()?.trim().orEmpty() @@ -653,6 +684,11 @@ class NodeRuntime(context: Context) { scope.launch { val trimmed = payloadJson.trim() if (trimmed.isEmpty()) return@launch + // Prevent untrusted pages (e.g. arbitrary websites) from escalating into agent actions. + // Only allow A2UI messages from the connected gateway canvas origin. + if (!isTrustedCanvasA2uiSource()) return@launch + // Avoid memory/DoS via huge payloads. + if (trimmed.length > 64 * 1024) return@launch val root = try { diff --git a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt b/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt index 673d60c8f..b95ef5244 100644 --- a/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt +++ b/apps/android/app/src/main/java/bot/molt/android/gateway/GatewayTls.kt @@ -5,6 +5,7 @@ import java.security.MessageDigest import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate +import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocketFactory @@ -62,7 +63,18 @@ fun buildGatewayTlsConfig( return GatewayTlsConfig( sslSocketFactory = context.socketFactory, trustManager = trustManager, - hostnameVerifier = HostnameVerifier { _, _ -> true }, + // Prefer hostname verification for DNS names (defense-in-depth for TOFU). + // Keep IP connections permissive since the gateway commonly uses self-signed + // certs and fingerprint pinning/TOFU is the primary binding. + hostnameVerifier = + HostnameVerifier { hostname, session -> + val h = hostname.trim() + if (h.isEmpty()) return@HostnameVerifier false + val isIpv4 = h.matches(Regex("^\\d{1,3}(?:\\.\\d{1,3}){3}$")) + val isIpv6 = h.contains(":") + if (isIpv4 || isIpv6) true + else HttpsURLConnection.getDefaultHostnameVerifier().verify(h, session) + }, ) } diff --git a/apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt b/apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt index 67d76b82f..679d621d7 100644 --- a/apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt +++ b/apps/android/app/src/main/java/bot/molt/android/ui/RootScreen.kt @@ -326,7 +326,15 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) settings.javaScriptEnabled = true // Some embedded web UIs (incl. the "background website") use localStorage/sessionStorage. settings.domStorageEnabled = true - settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE + // Security: avoid loading HTTP subresources into HTTPS pages. + settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW + settings.javaScriptCanOpenWindowsAutomatically = false + settings.setSupportMultipleWindows(false) + // Reduce file:// origin privilege (asset scaffold doesn't need cross-file access). + @Suppress("DEPRECATION") + settings.allowFileAccessFromFileURLs = false + @Suppress("DEPRECATION") + settings.allowUniversalAccessFromFileURLs = false if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false) } else { diff --git a/apps/macos/Sources/Moltbot/GatewayEndpointStore.swift b/apps/macos/Sources/Moltbot/GatewayEndpointStore.swift index 08c4249b0..3e33939d3 100644 --- a/apps/macos/Sources/Moltbot/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Moltbot/GatewayEndpointStore.swift @@ -634,18 +634,27 @@ extension GatewayEndpointStore { components.scheme = "http" } components.path = "/" - var queryItems: [URLQueryItem] = [] + // Security: never put credentials in URL query params (they end up in logs / history). + // Use a fragment instead; the browser can hydrate from it and immediately clean the URL. + var fragmentItems: [URLQueryItem] = [] if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty { - queryItems.append(URLQueryItem(name: "token", value: token)) + fragmentItems.append(URLQueryItem(name: "token", value: token)) } if let password = config.password?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty { - queryItems.append(URLQueryItem(name: "password", value: password)) + fragmentItems.append(URLQueryItem(name: "password", value: password)) + } + components.queryItems = nil + if fragmentItems.isEmpty { + components.fragment = nil + } else { + var frag = URLComponents() + frag.queryItems = fragmentItems + components.fragment = frag.percentEncodedQuery } - components.queryItems = queryItems.isEmpty ? nil : queryItems guard let url = components.url else { throw NSError(domain: "Dashboard", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Failed to build dashboard URL", diff --git a/docs/help/faq.md b/docs/help/faq.md index 7372a4997..467e827a1 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -323,13 +323,13 @@ The wizard now opens your browser with a tokenized dashboard URL right after onb **Localhost (same machine):** - Open `http://127.0.0.1:18789/`. -- If it asks for auth, run `moltbot dashboard` and use the tokenized link (`?token=...`). +- If it asks for auth, run `moltbot dashboard` and use the tokenized link (`#token=...`). - The token is the same value as `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) and is stored by the UI after first load. **Not on localhost:** - **Tailscale Serve** (recommended): keep bind loopback, run `moltbot gateway --tailscale serve`, open `https:///`. If `gateway.auth.allowTailscale` is `true`, identity headers satisfy auth (no token). - **Tailnet bind**: run `moltbot gateway --bind tailnet --token ""`, open `http://:18789/`, paste token in dashboard settings. -- **SSH tunnel**: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...` from `moltbot dashboard`. +- **SSH tunnel**: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/#token=...` from `moltbot dashboard`. See [Dashboard](/web/dashboard) and [Web surfaces](/web) for bind modes and auth details. @@ -2292,14 +2292,14 @@ Your gateway is running with auth enabled (`gateway.auth.*`), but the UI is not Facts (from code): - The Control UI stores the token in browser localStorage key `moltbot.control.settings.v1`. -- The UI can import `?token=...` (and/or `?password=...`) once, then strips it from the URL. +- The UI can import `#token=...` (and/or `#password=...`) once, then strips it from the URL. Fix: - Fastest: `moltbot dashboard` (prints + copies tokenized link, tries to open; shows SSH hint if headless). - If you don’t have a token yet: `moltbot doctor --generate-gateway-token`. -- If remote, tunnel first: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...`. +- If remote, tunnel first: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/#token=...`. - Set `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) on the gateway host. -- In the Control UI settings, paste the same token (or refresh with a one-time `?token=...` link). +- In the Control UI settings, paste the same token (or refresh with a one-time `#token=...` link). - Still stuck? Run `moltbot status --all` and follow [Troubleshooting](/gateway/troubleshooting). See [Dashboard](/web/dashboard) for auth details. ### I set gatewaybind tailnet but it cant bind nothing listens diff --git a/docs/platforms/exe-dev.md b/docs/platforms/exe-dev.md index 796ddc374..8ccd3203a 100644 --- a/docs/platforms/exe-dev.md +++ b/docs/platforms/exe-dev.md @@ -103,7 +103,7 @@ server { ## 5) Access Moltbot and grant privileges -Access `https://.exe.xyz/?token=YOUR-TOKEN-FROM-TERMINAL`. Approve +Access `https://.exe.xyz/#token=YOUR-TOKEN-FROM-TERMINAL`. Approve devices with `moltbot devices list` and `moltbot device approve`. When in doubt, use Shelley from your browser! diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md index 1fdd1937d..4b111a8fb 100644 --- a/docs/web/dashboard.md +++ b/docs/web/dashboard.md @@ -27,16 +27,16 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel. - After onboarding, the CLI now auto-opens the dashboard with your token and prints the same tokenized link. - Re-open anytime: `moltbot dashboard` (copies link, opens browser if possible, shows SSH hint if headless). -- The token stays local (query param only); the UI strips it after first load and saves it in localStorage. +- The token stays local (URL fragment only); the UI strips it after first load and saves it in localStorage. ## Token basics (local vs remote) -- **Localhost**: open `http://127.0.0.1:18789/`. If you see “unauthorized,” run `moltbot dashboard` and use the tokenized link (`?token=...`). +- **Localhost**: open `http://127.0.0.1:18789/`. If you see “unauthorized,” run `moltbot dashboard` and use the tokenized link (`#token=...`). - **Token source**: `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`); the UI stores it after first load. - **Not localhost**: use Tailscale Serve (tokenless if `gateway.auth.allowTailscale: true`), tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web). ## If you see “unauthorized” / 1008 - Run `moltbot dashboard` to get a fresh tokenized link. -- Ensure the gateway is reachable (local: `moltbot status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...`). +- Ensure the gateway is reachable (local: `moltbot status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/#token=...`). - In the dashboard settings, paste the same token you configured in `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`). diff --git a/package.json b/package.json index 4d38edf18..7e86efa89 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "express": "^5.2.1", "file-type": "^21.3.0", "grammy": "^1.39.3", - "hono": "4.11.4", + "hono": "4.11.7", "jiti": "^2.6.1", "json5": "^2.2.3", "jszip": "^3.10.1", @@ -201,7 +201,7 @@ "qrcode-terminal": "^0.12.0", "sharp": "^0.34.5", "sqlite-vec": "0.1.7-alpha.2", - "tar": "7.5.4", + "tar": "7.5.7", "tslog": "^4.10.2", "undici": "^7.19.0", "ws": "^8.19.0", @@ -242,14 +242,20 @@ "wireit": "^0.14.12" }, "overrides": { - "tar": "7.5.4" + "tar": "7.5.7", + "form-data": "2.5.4", + "qs": "6.14.1", + "tough-cookie": "4.1.3" }, "pnpm": { "minimumReleaseAge": 2880, "overrides": { "@sinclair/typebox": "0.34.47", - "hono": "4.11.4", - "tar": "7.5.4" + "hono": "4.11.7", + "tar": "7.5.7", + "form-data": "2.5.4", + "qs": "6.14.1", + "tough-cookie": "4.1.3" } }, "vitest": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c0f99928..62a8aa2cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,11 @@ settings: overrides: '@sinclair/typebox': 0.34.47 - hono: 4.11.4 - tar: 7.5.4 + hono: 4.11.7 + tar: 7.5.7 + form-data: 2.5.4 + qs: 6.14.1 + tough-cookie: 4.1.3 importers: @@ -21,7 +24,7 @@ importers: version: 3.975.0 '@buape/carbon': specifier: 0.14.0 - version: 0.14.0(hono@4.11.4) + version: 0.14.0(hono@4.11.7) '@clack/prompts': specifier: ^0.11.0 version: 0.11.0 @@ -110,8 +113,8 @@ importers: specifier: ^1.39.3 version: 1.39.3 hono: - specifier: 4.11.4 - version: 4.11.4 + specifier: 4.11.7 + version: 4.11.7 jiti: specifier: ^2.6.1 version: 2.6.1 @@ -155,8 +158,8 @@ importers: specifier: 0.1.7-alpha.2 version: 0.1.7-alpha.2 tar: - specifier: 7.5.4 - version: 7.5.4 + specifier: 7.5.7 + version: 7.5.7 tslog: specifier: ^4.10.2 version: 4.10.2 @@ -383,12 +386,12 @@ importers: '@microsoft/agents-hosting-extensions-teams': specifier: ^1.2.2 version: 1.2.2 - moltbot: - specifier: workspace:* - version: link:../.. express: specifier: ^5.2.1 version: 5.2.1 + moltbot: + specifier: workspace:* + version: link:../.. proper-lockfile: specifier: ^4.1.2 version: 4.1.2 @@ -1090,7 +1093,7 @@ packages: resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: 4.11.4 + hono: 4.11.7 '@huggingface/jinja@0.5.3': resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==} @@ -3214,11 +3217,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - clawdbot@2026.1.24-3: - resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==} - engines: {node: '>=22.12.0'} - hasBin: true - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -3632,17 +3630,10 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + form-data@2.5.4: + resolution: {integrity: sha512-Y/3MmRiR8Nd+0CUtrbvcKtKzLWiUfpQ7DFVggH8PwmGt/0r7RSy32GuP4hpCJlQNEBusisSx1DLtD8uD386HJQ==} engines: {node: '>= 0.12'} - - form-data@2.5.5: - resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} - engines: {node: '>= 0.12'} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} + deprecated: This version has an incorrect dependency; please use v2.5.5 formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} @@ -3764,6 +3755,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-own@1.0.1: + resolution: {integrity: sha512-RDKhzgQTQfMaLvIFhjahU+2gGnRBK6dYOd5Gd9BzkmnBneOCRYjRC003RIMrdAbH52+l+CnMS4bBCXGer8tEhg==} + deprecated: This project is not maintained. Use Object.hasOwn() instead. + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -3793,8 +3788,8 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} - hono@4.11.4: - resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} engines: {node: '>=16.9.0'} hookified@1.15.0: @@ -4809,9 +4804,8 @@ packages: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - qs@6.5.3: - resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} - engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4894,6 +4888,9 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -5190,8 +5187,8 @@ packages: tailwindcss@4.1.17: resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} - tar@7.5.4: - resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} + tar@7.5.7: + resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} thenify-all@1.6.0: @@ -5246,9 +5243,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@2.5.0: - resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} - engines: {node: '>=0.8'} + tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5334,6 +5331,10 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5351,6 +5352,9 @@ packages: url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -6433,14 +6437,14 @@ snapshots: '@borewit/text-codec@0.2.1': {} - '@buape/carbon@0.14.0(hono@4.11.4)': + '@buape/carbon@0.14.0(hono@4.11.7)': dependencies: '@types/node': 25.0.10 discord-api-types: 0.38.37 optionalDependencies: '@cloudflare/workers-types': 4.20260120.0 '@discordjs/voice': 0.19.0 - '@hono/node-server': 1.19.9(hono@4.11.4) + '@hono/node-server': 1.19.9(hono@4.11.7) '@types/bun': 1.3.6 '@types/ws': 8.18.1 ws: 8.19.0 @@ -6696,9 +6700,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@hono/node-server@1.19.9(hono@4.11.4)': + '@hono/node-server@1.19.9(hono@4.11.7)': dependencies: - hono: 4.11.4 + hono: 4.11.7 optional: true '@huggingface/jinja@0.5.3': @@ -7973,7 +7977,7 @@ snapshots: '@types/retry': 0.12.0 axios: 1.13.2(debug@4.4.3) eventemitter3: 5.0.4 - form-data: 4.0.5 + form-data: 2.5.4 is-electron: 2.2.2 is-stream: 2.0.1 p-queue: 6.6.2 @@ -8543,7 +8547,7 @@ snapshots: '@types/caseless': 0.12.5 '@types/node': 25.0.10 '@types/tough-cookie': 4.0.5 - form-data: 2.5.5 + form-data: 2.5.4 '@types/retry@0.12.0': {} @@ -8932,7 +8936,7 @@ snapshots: axios@1.13.2(debug@4.4.3): dependencies: follow-redirects: 1.15.11(debug@4.4.3) - form-data: 4.0.5 + form-data: 2.5.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -9098,84 +9102,6 @@ snapshots: dependencies: clsx: 2.1.1 - clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3): - dependencies: - '@agentclientprotocol/sdk': 0.13.1(zod@4.3.6) - '@aws-sdk/client-bedrock': 3.975.0 - '@buape/carbon': 0.14.0(hono@4.11.4) - '@clack/prompts': 0.11.0 - '@grammyjs/runner': 2.0.3(grammy@1.39.3) - '@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3) - '@homebridge/ciao': 1.3.4 - '@line/bot-sdk': 10.6.0 - '@lydell/node-pty': 1.2.0-beta.3 - '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.49.3 - '@mozilla/readability': 0.6.0 - '@sinclair/typebox': 0.34.47 - '@slack/bolt': 4.6.0(@types/express@5.0.6) - '@slack/web-api': 7.13.0 - '@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) - ajv: 8.17.1 - body-parser: 2.2.2 - chalk: 5.6.2 - chokidar: 5.0.0 - chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482) - cli-highlight: 2.1.11 - commander: 14.0.2 - croner: 9.1.0 - detect-libc: 2.1.2 - discord-api-types: 0.38.37 - dotenv: 17.2.3 - express: 5.2.1 - file-type: 21.3.0 - grammy: 1.39.3 - hono: 4.11.4 - jiti: 2.6.1 - json5: 2.2.3 - jszip: 3.10.1 - linkedom: 0.18.12 - long: 5.3.2 - markdown-it: 14.1.0 - node-edge-tts: 1.2.9 - osc-progress: 0.3.0 - pdfjs-dist: 5.4.530 - playwright-core: 1.58.0 - proper-lockfile: 4.1.2 - qrcode-terminal: 0.12.0 - sharp: 0.34.5 - sqlite-vec: 0.1.7-alpha.2 - tar: 7.5.4 - tslog: 4.10.2 - undici: 7.19.0 - ws: 8.19.0 - yaml: 2.8.2 - zod: 4.3.6 - optionalDependencies: - '@napi-rs/canvas': 0.1.88 - node-llama-cpp: 3.15.0(typescript@5.9.3) - transitivePeerDependencies: - - '@discordjs/opus' - - '@modelcontextprotocol/sdk' - - '@types/express' - - audio-decode - - aws-crt - - bufferutil - - canvas - - debug - - devtools-protocol - - encoding - - ffmpeg-static - - jimp - - link-preview-js - - node-opus - - opusscript - - supports-color - - typescript - - utf-8-validate - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -9217,7 +9143,7 @@ snapshots: npmlog: 6.0.2 rc: 1.2.8 semver: 7.7.3 - tar: 7.5.4 + tar: 7.5.7 url-join: 4.0.1 which: 2.0.2 yargs: 17.7.2 @@ -9650,29 +9576,15 @@ snapshots: forever-agent@0.6.1: {} - form-data@2.3.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - - form-data@2.5.5: + form-data@2.5.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + has-own: 1.0.1 mime-types: 2.1.35 safe-buffer: 5.2.1 - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -9825,6 +9737,8 @@ snapshots: has-flag@4.0.0: {} + has-own@1.0.1: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -9851,7 +9765,7 @@ snapshots: highlight.js@11.11.1: {} - hono@4.11.4: {} + hono@4.11.7: {} hookified@1.15.0: {} @@ -10930,7 +10844,7 @@ snapshots: dependencies: side-channel: 1.1.0 - qs@6.5.3: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -11025,7 +10939,7 @@ snapshots: request: 2.88.2 request-promise-core: 1.1.4(request@2.88.2) stealthy-require: 1.1.1 - tough-cookie: 2.5.0 + tough-cookie: 4.1.3 request@2.88.2: dependencies: @@ -11035,7 +10949,7 @@ snapshots: combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - form-data: 2.3.3 + form-data: 2.5.4 har-validator: 5.1.5 http-signature: 1.2.0 is-typedarray: 1.0.0 @@ -11044,9 +10958,9 @@ snapshots: mime-types: 2.1.35 oauth-sign: 0.9.0 performance-now: 2.1.0 - qs: 6.5.3 + qs: 6.14.1 safe-buffer: 5.2.1 - tough-cookie: 2.5.0 + tough-cookie: 4.1.3 tunnel-agent: 0.6.0 uuid: 3.4.0 @@ -11061,6 +10975,8 @@ snapshots: transitivePeerDependencies: - supports-color + requires-port@1.0.0: {} + resolve-pkg-maps@1.0.0: {} restore-cursor@5.1.0: @@ -11470,7 +11386,7 @@ snapshots: tailwindcss@4.1.17: {} - tar@7.5.4: + tar@7.5.7: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -11522,10 +11438,12 @@ snapshots: totalist@3.0.1: {} - tough-cookie@2.5.0: + tough-cookie@4.1.3: dependencies: psl: 1.15.0 punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 tr46@0.0.3: {} @@ -11599,6 +11517,8 @@ snapshots: universal-user-agent@7.0.3: optional: true + universalify@0.2.0: {} + universalify@2.0.1: optional: true @@ -11613,6 +11533,11 @@ snapshots: url-join@4.0.1: optional: true + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} diff --git a/src/commands/dashboard.test.ts b/src/commands/dashboard.test.ts index acb736823..37e151515 100644 --- a/src/commands/dashboard.test.ts +++ b/src/commands/dashboard.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { dashboardCommand } from "./dashboard.js"; +import type { RuntimeEnv } from "../runtime.js"; const mocks = vi.hoisted(() => ({ readConfigFileSnapshot: vi.fn(), @@ -31,8 +32,10 @@ vi.mock("../infra/clipboard.js", () => ({ const runtime = { log: vi.fn(), error: vi.fn(), - exit: vi.fn(), -}; + exit: vi.fn((_code?: number): never => { + throw new Error("exit"); + }), +} satisfies RuntimeEnv; function resetRuntime() { runtime.log.mockClear(); @@ -84,8 +87,8 @@ describe("dashboardCommand", () => { customBindHost: undefined, basePath: undefined, }); - expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/?token=abc123"); - expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/?token=abc123"); + expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123"); + expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123"); expect(runtime.log).toHaveBeenCalledWith( "Opened in your browser. Keep that tab to control Moltbot.", ); diff --git a/src/commands/dashboard.ts b/src/commands/dashboard.ts index b5fb09309..153869d1a 100644 --- a/src/commands/dashboard.ts +++ b/src/commands/dashboard.ts @@ -31,7 +31,14 @@ export async function dashboardCommand( customBindHost, basePath, }); - const authedUrl = token ? `${links.httpUrl}?token=${encodeURIComponent(token)}` : links.httpUrl; + const authedUrl = (() => { + if (!token) return links.httpUrl; + const url = new URL(links.httpUrl); + const params = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash); + params.set("token", token); + url.hash = params.toString(); + return url.toString(); + })(); runtime.log(`Dashboard URL: ${authedUrl}`); diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 376555a39..e8fbfd482 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -168,8 +168,14 @@ export function formatControlUiSshHint(params: { const basePath = normalizeControlUiBasePath(params.basePath); const uiPath = basePath ? `${basePath}/` : "/"; const localUrl = `http://localhost:${params.port}${uiPath}`; - const tokenParam = params.token ? `?token=${encodeURIComponent(params.token)}` : ""; - const authedUrl = params.token ? `${localUrl}${tokenParam}` : undefined; + const authedUrl = (() => { + if (!params.token) return undefined; + const url = new URL(localUrl); + const frag = new URLSearchParams(); + frag.set("token", params.token); + url.hash = frag.toString(); + return url.toString(); + })(); const sshTarget = resolveSshTargetHint(); return [ "No GUI detected. Open from your computer:", diff --git a/src/config/schema.ts b/src/config/schema.ts index 28c994f3d..b8ba54e58 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -202,6 +202,7 @@ const FIELD_LABELS: Record = { "gateway.controlUi.basePath": "Control UI Base Path", "gateway.controlUi.allowInsecureAuth": "Allow Insecure Control UI Auth", "gateway.controlUi.dangerouslyDisableDeviceAuth": "Dangerously Disable Control UI Device Auth", + "gateway.plugins.http.protectApiPaths": "Protect Plugin API Paths", "gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint", "gateway.reload.mode": "Config Reload Mode", "gateway.reload.debounceMs": "Config Reload Debounce (ms)", @@ -388,6 +389,8 @@ const FIELD_HELP: Record = { "Allow Control UI auth over insecure HTTP (token-only; not recommended).", "gateway.controlUi.dangerouslyDisableDeviceAuth": "DANGEROUS. Disable Control UI device identity checks (token/password only).", + "gateway.plugins.http.protectApiPaths": + "Require Gateway auth for plugin-provided endpoints under `/api/**` (recommended when exposed beyond localhost).", "gateway.http.endpoints.chatCompletions.enabled": "Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).", "gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).', diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index a0d562f7b..2a1d50279 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -191,6 +191,19 @@ export type GatewayHttpConfig = { endpoints?: GatewayHttpEndpointsConfig; }; +export type GatewayPluginsHttpConfig = { + /** + * Protect plugin-provided API endpoints under `/api/**` with Gateway auth. + * This is enabled by default because plugins can register HTTP handlers that + * mutate config or trigger agent actions. + */ + protectApiPaths?: boolean; +}; + +export type GatewayPluginsConfig = { + http?: GatewayPluginsHttpConfig; +}; + export type GatewayNodesConfig = { /** Browser routing policy for node-hosted browser proxies. */ browser?: { @@ -232,6 +245,7 @@ export type GatewayConfig = { reload?: GatewayReloadConfig; tls?: GatewayTlsConfig; http?: GatewayHttpConfig; + plugins?: GatewayPluginsConfig; nodes?: GatewayNodesConfig; /** * IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index ce4115517..9331a5de4 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -426,6 +426,17 @@ export const MoltbotSchema = z }) .strict() .optional(), + plugins: z + .object({ + http: z + .object({ + protectApiPaths: z.boolean().optional(), + }) + .strict() + .optional(), + }) + .strict() + .optional(), nodes: z .object({ browser: z diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index b06092a96..a69ba294a 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -76,6 +76,20 @@ function contentTypeForExt(ext: string): string { } } +function setControlUiSecurityHeaders(res: ServerResponse) { + // Control UI is a local-first surface. These headers provide defense-in-depth + // against token leakage and UI embedding attacks. + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("Referrer-Policy", "no-referrer"); + res.setHeader("X-Frame-Options", "DENY"); + // Keep CSP minimal to avoid breaking the UI build (it injects an inline config script). + // We still prevent embedding + plugin/object injection. + res.setHeader( + "Content-Security-Policy", + "frame-ancestors 'none'; base-uri 'none'; object-src 'none'", + ); +} + export type ControlUiAvatarResolution = | { kind: "none"; reason: string } | { kind: "local"; filePath: string } @@ -88,6 +102,7 @@ type ControlUiAvatarMeta = { function sendJson(res: ServerResponse, status: number, body: unknown) { res.statusCode = status; + setControlUiSecurityHeaders(res); res.setHeader("Content-Type", "application/json; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); res.end(JSON.stringify(body)); @@ -141,6 +156,7 @@ export function handleControlUiAvatarRequest( if (req.method === "HEAD") { res.statusCode = 200; + setControlUiSecurityHeaders(res); res.setHeader("Content-Type", contentTypeForExt(path.extname(resolved.filePath).toLowerCase())); res.setHeader("Cache-Control", "no-cache"); res.end(); @@ -153,12 +169,14 @@ export function handleControlUiAvatarRequest( function respondNotFound(res: ServerResponse) { res.statusCode = 404; + setControlUiSecurityHeaders(res); res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("Not Found"); } function serveFile(res: ServerResponse, filePath: string) { const ext = path.extname(filePath).toLowerCase(); + setControlUiSecurityHeaders(res); res.setHeader("Content-Type", contentTypeForExt(ext)); // Static UI should never be cached aggressively while iterating; allow the // browser to revalidate. @@ -214,6 +232,7 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex agentId: resolvedAgentId, basePath, }) ?? identity.avatar; + setControlUiSecurityHeaders(res); res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); const raw = fs.readFileSync(indexPath, "utf8"); diff --git a/src/gateway/server-runtime-state.ts b/src/gateway/server-runtime-state.ts index 02b060a3b..b95ddba75 100644 --- a/src/gateway/server-runtime-state.ts +++ b/src/gateway/server-runtime-state.ts @@ -102,6 +102,9 @@ export async function createGatewayRuntimeState(params: { const handlePluginRequest = createGatewayPluginRequestHandler({ registry: params.pluginRegistry, log: params.logPlugins, + auth: params.resolvedAuth, + trustedProxies: params.cfg.gateway?.trustedProxies, + protectApiPaths: params.cfg.gateway?.plugins?.http?.protectApiPaths ?? true, }); const bindHosts = await resolveGatewayListenHosts(params.bindHost); diff --git a/src/gateway/server/plugins-http.ts b/src/gateway/server/plugins-http.ts index f8a7f85fd..7ae3b6416 100644 --- a/src/gateway/server/plugins-http.ts +++ b/src/gateway/server/plugins-http.ts @@ -2,6 +2,10 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { createSubsystemLogger } from "../../logging/subsystem.js"; import type { PluginRegistry } from "../../plugins/registry.js"; +import type { ResolvedGatewayAuth } from "../auth.js"; +import { authorizeGatewayConnect } from "../auth.js"; +import { sendUnauthorized } from "../http-common.js"; +import { getBearerToken, getHeader } from "../http-utils.js"; type SubsystemLogger = ReturnType; @@ -13,6 +17,9 @@ export type PluginHttpRequestHandler = ( export function createGatewayPluginRequestHandler(params: { registry: PluginRegistry; log: SubsystemLogger; + auth?: ResolvedGatewayAuth; + trustedProxies?: string[]; + protectApiPaths?: boolean; }): PluginHttpRequestHandler { const { registry, log } = params; return async (req, res) => { @@ -20,8 +27,30 @@ export function createGatewayPluginRequestHandler(params: { const handlers = registry.httpHandlers ?? []; if (routes.length === 0 && handlers.length === 0) return false; + const url = new URL(req.url ?? "/", "http://localhost"); + + // Security hardening: by default, treat `/api/**` as an authenticated surface. + // Plugins may expose config-mutating endpoints under this namespace. + if (params.protectApiPaths !== false && url.pathname.startsWith("/api/")) { + const token = getBearerToken(req) ?? getHeader(req, "x-moltbot-token")?.trim() ?? ""; + const auth = params.auth; + if (!auth) { + sendUnauthorized(res); + return true; + } + const authResult = await authorizeGatewayConnect({ + auth, + connectAuth: token ? { token, password: token } : null, + req, + trustedProxies: params.trustedProxies, + }); + if (!authResult.ok) { + sendUnauthorized(res); + return true; + } + } + if (routes.length > 0) { - const url = new URL(req.url ?? "/", "http://localhost"); const route = routes.find((entry) => entry.path === url.pathname); if (route) { try { diff --git a/src/infra/archive.ts b/src/infra/archive.ts index 35ad4fa04..8f2297cb1 100644 --- a/src/infra/archive.ts +++ b/src/infra/archive.ts @@ -101,7 +101,32 @@ export async function extractArchive(params: { const label = kind === "zip" ? "extract zip" : "extract tar"; if (kind === "tar") { await withTimeout( - tar.x({ file: params.archivePath, cwd: params.destDir }), + tar.x({ + file: params.archivePath, + cwd: params.destDir, + // Defense-in-depth: prevent path traversal and link-based extraction attacks. + // (We only need regular files + dirs for npm/plugin archives.) + preservePaths: false, + strict: true, + filter: (entryPath, entry) => { + const normalized = String(entryPath ?? "").replaceAll("\\", "/"); + if (!normalized) return false; + if (normalized.startsWith("/") || normalized === ".." || normalized.startsWith("../")) { + params.logger?.warn?.(`blocked tar entry (path escape): ${entryPath}`); + return false; + } + if (normalized.includes("/../")) { + params.logger?.warn?.(`blocked tar entry (path traversal): ${entryPath}`); + return false; + } + const type = String((entry as any)?.type ?? ""); + if (type === "SymbolicLink" || type === "Link") { + params.logger?.warn?.(`blocked tar entry (${type}): ${entryPath}`); + return false; + } + return true; + }, + }), params.timeoutMs, label, ); diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 96a4a4bf6..49da7c2e1 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -249,11 +249,14 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption customBindHost: settings.customBindHost, basePath: controlUiBasePath, }); - const tokenParam = - settings.authMode === "token" && settings.gatewayToken - ? `?token=${encodeURIComponent(settings.gatewayToken)}` - : ""; - const authedUrl = `${links.httpUrl}${tokenParam}`; + const authedUrl = (() => { + if (settings.authMode !== "token" || !settings.gatewayToken) return ""; + const url = new URL(links.httpUrl); + const frag = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash); + frag.set("token", settings.gatewayToken); + url.hash = frag.toString(); + return url.toString(); + })(); const gatewayProbe = await probeGatewayReachable({ url: links.wsUrl, token: settings.authMode === "token" ? settings.gatewayToken : undefined, @@ -274,7 +277,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption await prompter.note( [ `Web UI: ${links.httpUrl}`, - tokenParam ? `Web UI (with token): ${authedUrl}` : undefined, + authedUrl ? `Web UI (with token): ${authedUrl}` : undefined, `Gateway WS: ${links.wsUrl}`, gatewayStatusLine, "Docs: https://docs.molt.bot/web/control-ui", diff --git a/ui/src/ui/app-channels.ts b/ui/src/ui/app-channels.ts index 91ff734ed..2579f03c1 100644 --- a/ui/src/ui/app-channels.ts +++ b/ui/src/ui/app-channels.ts @@ -115,10 +115,12 @@ export async function handleNostrProfileSave(host: MoltbotApp) { }; try { + const auth = host.settings.token.trim() ? host.settings.token.trim() : host.password.trim(); const response = await fetch(buildNostrProfileUrl(accountId), { method: "PUT", headers: { "Content-Type": "application/json", + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), }, body: JSON.stringify(state.values), }); @@ -180,10 +182,12 @@ export async function handleNostrProfileImport(host: MoltbotApp) { }; try { + const auth = host.settings.token.trim() ? host.settings.token.trim() : host.password.trim(); const response = await fetch(buildNostrProfileUrl(accountId, "/import"), { method: "POST", headers: { "Content-Type": "application/json", + ...(auth ? { Authorization: `Bearer ${auth}` } : {}), }, body: JSON.stringify({ autoMerge: true }), }); diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 7e3ab29cf..ea9aa0650 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -58,12 +58,24 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) { } export function applySettingsFromUrl(host: SettingsHost) { - if (!window.location.search) return; - const params = new URLSearchParams(window.location.search); - const tokenRaw = params.get("token"); - const passwordRaw = params.get("password"); - const sessionRaw = params.get("session"); - const gatewayUrlRaw = params.get("gatewayUrl"); + const hasQuery = Boolean(window.location.search); + const hasHash = Boolean(window.location.hash); + if (!hasQuery && !hasHash) return; + + const url = new URL(window.location.href); + const queryParams = new URLSearchParams(url.search); + const fragmentRaw = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash; + const fragmentParams = new URLSearchParams(fragmentRaw); + + // Security: accept sensitive fields ONLY from the URL fragment. + // Query params are sent to the server and commonly end up in logs and analytics. + const tokenRaw = fragmentParams.get("token"); + const passwordRaw = fragmentParams.get("password"); + const gatewayUrlRaw = fragmentParams.get("gatewayUrl"); + + // Session is not a secret; accept from either query or fragment. + const sessionRaw = queryParams.get("session") ?? fragmentParams.get("session"); + let shouldCleanUrl = false; if (tokenRaw != null) { @@ -71,7 +83,7 @@ export function applySettingsFromUrl(host: SettingsHost) { if (token && token !== host.settings.token) { applySettings(host, { ...host.settings, token }); } - params.delete("token"); + fragmentParams.delete("token"); shouldCleanUrl = true; } @@ -80,7 +92,7 @@ export function applySettingsFromUrl(host: SettingsHost) { if (password) { (host as { password: string }).password = password; } - params.delete("password"); + fragmentParams.delete("password"); shouldCleanUrl = true; } @@ -94,6 +106,7 @@ export function applySettingsFromUrl(host: SettingsHost) { lastActiveSessionKey: session, }); } + // Keep session in the URL by default; don't force-clean it. } if (gatewayUrlRaw != null) { @@ -101,13 +114,22 @@ export function applySettingsFromUrl(host: SettingsHost) { if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) { host.pendingGatewayUrl = gatewayUrl; } - params.delete("gatewayUrl"); + fragmentParams.delete("gatewayUrl"); + shouldCleanUrl = true; + } + + // Opportunistically strip sensitive query params too (legacy links). + if (queryParams.has("token") || queryParams.has("password") || queryParams.has("gatewayUrl")) { + queryParams.delete("token"); + queryParams.delete("password"); + queryParams.delete("gatewayUrl"); shouldCleanUrl = true; } if (!shouldCleanUrl) return; - const url = new URL(window.location.href); - url.search = params.toString(); + url.search = queryParams.toString(); + const nextFragment = fragmentParams.toString(); + url.hash = nextFragment ? `#${nextFragment}` : ""; window.history.replaceState({}, "", url.toString()); } diff --git a/ui/src/ui/navigation.browser.test.ts b/ui/src/ui/navigation.browser.test.ts index 7a021e64f..7925991cb 100644 --- a/ui/src/ui/navigation.browser.test.ts +++ b/ui/src/ui/navigation.browser.test.ts @@ -149,34 +149,37 @@ describe("control UI routing", () => { expect(container.scrollTop).toBe(maxScroll); }); - it("hydrates token from URL params and strips it", async () => { - const app = mountApp("/ui/overview?token=abc123"); + it("hydrates token from URL fragment and strips it", async () => { + const app = mountApp("/ui/overview#token=abc123"); await app.updateComplete; expect(app.settings.token).toBe("abc123"); expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.search).toBe(""); + expect(window.location.hash).toBe(""); }); - it("hydrates password from URL params and strips it", async () => { - const app = mountApp("/ui/overview?password=sekret"); + it("hydrates password from URL fragment and strips it", async () => { + const app = mountApp("/ui/overview#password=sekret"); await app.updateComplete; expect(app.password).toBe("sekret"); expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.search).toBe(""); + expect(window.location.hash).toBe(""); }); - it("hydrates token from URL params even when settings already set", async () => { + it("hydrates token from URL fragment even when settings already set", async () => { localStorage.setItem( "moltbot.control.settings.v1", JSON.stringify({ token: "existing-token" }), ); - const app = mountApp("/ui/overview?token=abc123"); + const app = mountApp("/ui/overview#token=abc123"); await app.updateComplete; expect(app.settings.token).toBe("abc123"); expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.search).toBe(""); + expect(window.location.hash).toBe(""); }); });