security: harden credential handling, API auth, and archive extraction

- Control UI: switch token/password from query params to URL fragments (#token=...)
  - Auto-strips after first load, never logged in server access logs
  - Added defense-in-depth headers (Referrer-Policy, X-Frame-Options, CSP, nosniff)
- macOS: "Open Dashboard" now uses fragments instead of query params
- CLI/onboarding: emit fragment links instead of query param links
- Plugin HTTP: /api/** now requires Gateway auth (fixes unauthenticated Nostr API)
  - Added config toggle gateway.plugins.http.protectApiPaths (default: true)
- Control UI: sends Authorization header for Nostr profile save/import
- Android hardening:
  - WebView: disabled mixed content, multi-window, reduced file URL privileges
  - A2UI bridge: origin validation + 64KB payload cap
  - TLS: enabled hostname verification for DNS names
- Archive extraction: block path traversal + symlink/hardlink entries
- Dependencies: upgraded tar 7.5.7, hono 4.11.7, added overrides for vulnerabilities

Breaking: Old ?token=... dashboard links no longer auto-auth; use #token=... instead
This commit is contained in:
VihariKanukollu 2026-01-29 16:05:38 +05:30
parent 6372242da7
commit cbbe9dd0a2
24 changed files with 711 additions and 302 deletions

View File

@ -146,6 +146,22 @@
} }
], ],
"results": { "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": [ ".env.example": [
{ {
"type": "Twilio API Key", "type": "Twilio API Key",
@ -156,75 +172,75 @@
} }
], ],
"appcast.xml": [ "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", "type": "Base64 High Entropy String",
"filename": "appcast.xml", "filename": "appcast.xml",
"hashed_secret": "4e5f0a148d9ef42afeb73b1c77643e2ef2dee0b9", "hashed_secret": "4e5f0a148d9ef42afeb73b1c77643e2ef2dee0b9",
"is_verified": false, "is_verified": false,
"line_number": 90 "line_number": 185
},
{
"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
} }
], ],
"apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift": [ "apps/macos/Tests/MoltbotIPCTests/AnthropicAuthResolverTests.swift": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift", "filename": "apps/macos/Tests/MoltbotIPCTests/AnthropicAuthResolverTests.swift",
"hashed_secret": "e761624445731fcb8b15da94343c6b92e507d190", "hashed_secret": "e761624445731fcb8b15da94343c6b92e507d190",
"is_verified": false, "is_verified": false,
"line_number": 26 "line_number": 26
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift", "filename": "apps/macos/Tests/MoltbotIPCTests/AnthropicAuthResolverTests.swift",
"hashed_secret": "a23c8630c8a5fbaa21f095e0269c135c20d21689", "hashed_secret": "a23c8630c8a5fbaa21f095e0269c135c20d21689",
"is_verified": false, "is_verified": false,
"line_number": 42 "line_number": 42
} }
], ],
"apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift": [ "apps/macos/Tests/MoltbotIPCTests/GatewayEndpointStoreTests.swift": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift", "filename": "apps/macos/Tests/MoltbotIPCTests/GatewayEndpointStoreTests.swift",
"hashed_secret": "19dad5cecb110281417d1db56b60e1b006d55bb4", "hashed_secret": "19dad5cecb110281417d1db56b60e1b006d55bb4",
"is_verified": false, "is_verified": false,
"line_number": 61 "line_number": 61
} }
], ],
"apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift": [ "apps/macos/Tests/MoltbotIPCTests/GatewayLaunchAgentManagerTests.swift": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift", "filename": "apps/macos/Tests/MoltbotIPCTests/GatewayLaunchAgentManagerTests.swift",
"hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073",
"is_verified": false, "is_verified": false,
"line_number": 13 "line_number": 13
} }
], ],
"apps/macos/Tests/ClawdbotIPCTests/TailscaleIntegrationSectionTests.swift": [ "apps/macos/Tests/MoltbotIPCTests/TailscaleIntegrationSectionTests.swift": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "apps/macos/Tests/ClawdbotIPCTests/TailscaleIntegrationSectionTests.swift", "filename": "apps/macos/Tests/MoltbotIPCTests/TailscaleIntegrationSectionTests.swift",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false, "is_verified": false,
"line_number": 27 "line_number": 27
} }
], ],
"apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift": [ "apps/shared/MoltbotKit/Sources/MoltbotKit/GatewayChannel.swift": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift", "filename": "apps/shared/MoltbotKit/Sources/MoltbotKit/GatewayChannel.swift",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false, "is_verified": false,
"line_number": 100 "line_number": 100
@ -248,6 +264,15 @@
"line_number": 32 "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": [ "docs/channels/matrix.md": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
@ -284,27 +309,43 @@
"line_number": 141 "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": [ "docs/concepts/memory.md": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/concepts/memory.md", "filename": "docs/concepts/memory.md",
"hashed_secret": "39d711243bfcee9fec8299b204e1aa9c3430fa12", "hashed_secret": "39d711243bfcee9fec8299b204e1aa9c3430fa12",
"is_verified": false, "is_verified": false,
"line_number": 108 "line_number": 132
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/concepts/memory.md", "filename": "docs/concepts/memory.md",
"hashed_secret": "1a8abbf465c52363ab4c9c6ad945b8e857cbea55", "hashed_secret": "1a8abbf465c52363ab4c9c6ad945b8e857cbea55",
"is_verified": false, "is_verified": false,
"line_number": 131 "line_number": 155
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/concepts/memory.md", "filename": "docs/concepts/memory.md",
"hashed_secret": "b9f640d6095b9f6b5a65983f7b76dbbb254e0044", "hashed_secret": "b9f640d6095b9f6b5a65983f7b76dbbb254e0044",
"is_verified": false, "is_verified": false,
"line_number": 373 "line_number": 397
} }
], ],
"docs/concepts/model-providers.md": [ "docs/concepts/model-providers.md": [
@ -313,14 +354,14 @@
"filename": "docs/concepts/model-providers.md", "filename": "docs/concepts/model-providers.md",
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
"is_verified": false, "is_verified": false,
"line_number": 168 "line_number": 171
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/concepts/model-providers.md", "filename": "docs/concepts/model-providers.md",
"hashed_secret": "ef83ad68b9b66e008727b7c417c6a8f618b5177e", "hashed_secret": "ef83ad68b9b66e008727b7c417c6a8f618b5177e",
"is_verified": false, "is_verified": false,
"line_number": 255 "line_number": 282
} }
], ],
"docs/environment.md": [ "docs/environment.md": [
@ -359,21 +400,21 @@
"filename": "docs/gateway/configuration-examples.md", "filename": "docs/gateway/configuration-examples.md",
"hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27",
"is_verified": false, "is_verified": false,
"line_number": 319 "line_number": 320
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration-examples.md", "filename": "docs/gateway/configuration-examples.md",
"hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd",
"is_verified": false, "is_verified": false,
"line_number": 414 "line_number": 415
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration-examples.md", "filename": "docs/gateway/configuration-examples.md",
"hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209", "hashed_secret": "16c249e04e2be318050cb883c40137361c0c7209",
"is_verified": false, "is_verified": false,
"line_number": 548 "line_number": 549
} }
], ],
"docs/gateway/configuration.md": [ "docs/gateway/configuration.md": [
@ -382,63 +423,63 @@
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281",
"is_verified": false, "is_verified": false,
"line_number": 272 "line_number": 283
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25",
"is_verified": false, "is_verified": false,
"line_number": 274 "line_number": 285
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false, "is_verified": false,
"line_number": 1029 "line_number": 1037
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e",
"is_verified": false, "is_verified": false,
"line_number": 1470 "line_number": 1519
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770",
"is_verified": false, "is_verified": false,
"line_number": 1486 "line_number": 1535
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27",
"is_verified": false, "is_verified": false,
"line_number": 2268 "line_number": 2320
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
"is_verified": false, "is_verified": false,
"line_number": 2344 "line_number": 2396
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd",
"is_verified": false, "is_verified": false,
"line_number": 2658 "line_number": 2711
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/gateway/configuration.md", "filename": "docs/gateway/configuration.md",
"hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6",
"is_verified": false, "is_verified": false,
"line_number": 2844 "line_number": 2907
} }
], ],
"docs/gateway/local-models.md": [ "docs/gateway/local-models.md": [
@ -463,7 +504,7 @@
"filename": "docs/gateway/tailscale.md", "filename": "docs/gateway/tailscale.md",
"hashed_secret": "9cb0dc5383312aa15b9dc6745645bde18ff5ade9", "hashed_secret": "9cb0dc5383312aa15b9dc6745645bde18ff5ade9",
"is_verified": false, "is_verified": false,
"line_number": 75 "line_number": 78
} }
], ],
"docs/help/faq.md": [ "docs/help/faq.md": [
@ -472,35 +513,35 @@
"filename": "docs/help/faq.md", "filename": "docs/help/faq.md",
"hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac",
"is_verified": false, "is_verified": false,
"line_number": 925 "line_number": 1333
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/help/faq.md", "filename": "docs/help/faq.md",
"hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281",
"is_verified": false, "is_verified": false,
"line_number": 1113 "line_number": 1595
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/help/faq.md", "filename": "docs/help/faq.md",
"hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25",
"is_verified": false, "is_verified": false,
"line_number": 1114 "line_number": 1596
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/help/faq.md", "filename": "docs/help/faq.md",
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
"is_verified": false, "is_verified": false,
"line_number": 1439 "line_number": 2007
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/help/faq.md", "filename": "docs/help/faq.md",
"hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6",
"is_verified": false, "is_verified": false,
"line_number": 1715 "line_number": 2279
} }
], ],
"docs/nodes/talk.md": [ "docs/nodes/talk.md": [
@ -521,13 +562,40 @@
"line_number": 35 "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": [ "docs/providers/anthropic.md": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/providers/anthropic.md", "filename": "docs/providers/anthropic.md",
"hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36",
"is_verified": false, "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": [ "docs/providers/glm.md": [
@ -561,7 +629,16 @@
"filename": "docs/providers/moonshot.md", "filename": "docs/providers/moonshot.md",
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
"is_verified": false, "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": [ "docs/providers/openai.md": [
@ -570,7 +647,7 @@
"filename": "docs/providers/openai.md", "filename": "docs/providers/openai.md",
"hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0",
"is_verified": false, "is_verified": false,
"line_number": 31 "line_number": 29
} }
], ],
"docs/providers/opencode.md": [ "docs/providers/opencode.md": [
@ -600,6 +677,22 @@
"line_number": 31 "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": [ "docs/providers/zai.md": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
@ -615,7 +708,7 @@
"filename": "docs/tools/browser.md", "filename": "docs/tools/browser.md",
"hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684",
"is_verified": false, "is_verified": false,
"line_number": 163 "line_number": 136
} }
], ],
"docs/tools/firecrawl.md": [ "docs/tools/firecrawl.md": [
@ -642,7 +735,7 @@
"filename": "docs/tools/skills.md", "filename": "docs/tools/skills.md",
"hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd",
"is_verified": false, "is_verified": false,
"line_number": 160 "line_number": 168
} }
], ],
"docs/tools/web.md": [ "docs/tools/web.md": [
@ -672,7 +765,7 @@
"filename": "docs/tools/web.md", "filename": "docs/tools/web.md",
"hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217",
"is_verified": false, "is_verified": false,
"line_number": 223 "line_number": 230
} }
], ],
"docs/tts.md": [ "docs/tts.md": [
@ -681,14 +774,14 @@
"filename": "docs/tts.md", "filename": "docs/tts.md",
"hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770", "hashed_secret": "bde4db9b4c3be4049adc3b9a69851d7c35119770",
"is_verified": false, "is_verified": false,
"line_number": 72 "line_number": 93
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "docs/tts.md", "filename": "docs/tts.md",
"hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e", "hashed_secret": "1188d5a8ed7edcff5144a9472af960243eacf12e",
"is_verified": false, "is_verified": false,
"line_number": 77 "line_number": 98
} }
], ],
"extensions/bluebubbles/src/actions.test.ts": [ "extensions/bluebubbles/src/actions.test.ts": [
@ -780,14 +873,14 @@
"filename": "extensions/bluebubbles/src/monitor.test.ts", "filename": "extensions/bluebubbles/src/monitor.test.ts",
"hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc",
"is_verified": false, "is_verified": false,
"line_number": 187 "line_number": 193
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "extensions/bluebubbles/src/monitor.test.ts", "filename": "extensions/bluebubbles/src/monitor.test.ts",
"hashed_secret": "1ae0af3fe72b3ba394f9fa95a6cffc090d726c23", "hashed_secret": "1ae0af3fe72b3ba394f9fa95a6cffc090d726c23",
"is_verified": false, "is_verified": false,
"line_number": 394 "line_number": 400
} }
], ],
"extensions/bluebubbles/src/reactions.test.ts": [ "extensions/bluebubbles/src/reactions.test.ts": [
@ -833,7 +926,7 @@
"filename": "extensions/bluebubbles/src/send.test.ts", "filename": "extensions/bluebubbles/src/send.test.ts",
"hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d", "hashed_secret": "faacad0ce4ea1c19b46e128fd79679d37d3d331d",
"is_verified": false, "is_verified": false,
"line_number": 675 "line_number": 767
} }
], ],
"extensions/bluebubbles/src/targets.test.ts": [ "extensions/bluebubbles/src/targets.test.ts": [
@ -872,6 +965,15 @@
"line_number": 9 "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": [ "extensions/matrix/src/matrix/accounts.test.ts": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
@ -955,7 +1057,7 @@
"filename": "extensions/nextcloud-talk/src/channel.ts", "filename": "extensions/nextcloud-talk/src/channel.ts",
"hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb",
"is_verified": false, "is_verified": false,
"line_number": 390 "line_number": 391
} }
], ],
"extensions/nostr/README.md": [ "extensions/nostr/README.md": [
@ -1109,6 +1211,40 @@
"line_number": 198 "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": [ "extensions/zalo/README.md": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
@ -1169,7 +1305,7 @@
"filename": "src/agents/memory-search.test.ts", "filename": "src/agents/memory-search.test.ts",
"hashed_secret": "a1b49d68a91fdf9c9217773f3fac988d77fa0f50", "hashed_secret": "a1b49d68a91fdf9c9217773f3fac988d77fa0f50",
"is_verified": false, "is_verified": false,
"line_number": 164 "line_number": 187
} }
], ],
"src/agents/model-auth.test.ts": [ "src/agents/model-auth.test.ts": [
@ -1281,14 +1417,14 @@
"filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts",
"hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c",
"is_verified": false, "is_verified": false,
"line_number": 112 "line_number": 120
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts", "filename": "src/agents/models-config.skips-writing-models-json-no-env-token.test.ts",
"hashed_secret": "94c4be5a1976115e8152960c21e04400a4fccdf6", "hashed_secret": "94c4be5a1976115e8152960c21e04400a4fccdf6",
"is_verified": false, "is_verified": false,
"line_number": 146 "line_number": 154
} }
], ],
"src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts": [ "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", "filename": "src/agents/pi-embedded-runner.test.ts",
"hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd", "hashed_secret": "e9a5f12a8ecbb3eb46eca5096b5c52aa5e7c9fdd",
"is_verified": false, "is_verified": false,
"line_number": 117 "line_number": 118
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/agents/pi-embedded-runner.test.ts", "filename": "src/agents/pi-embedded-runner.test.ts",
"hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c", "hashed_secret": "fcdd655b11f33ba4327695084a347b2ba192976c",
"is_verified": false, "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": [ "src/agents/skills.applyskillenvoverrides.test.ts": [
@ -1446,28 +1591,28 @@
"filename": "src/agents/tools/web-search.ts", "filename": "src/agents/tools/web-search.ts",
"hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b", "hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b",
"is_verified": false, "is_verified": false,
"line_number": 85 "line_number": 93
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/agents/tools/web-search.ts", "filename": "src/agents/tools/web-search.ts",
"hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb", "hashed_secret": "71f8e7976e4cbc4561c9d62fb283e7f788202acb",
"is_verified": false, "is_verified": false,
"line_number": 190 "line_number": 198
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/agents/tools/web-search.ts", "filename": "src/agents/tools/web-search.ts",
"hashed_secret": "c4865ff9250aca23b0d98eb079dad70ebec1cced", "hashed_secret": "c4865ff9250aca23b0d98eb079dad70ebec1cced",
"is_verified": false, "is_verified": false,
"line_number": 198 "line_number": 206
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/agents/tools/web-search.ts", "filename": "src/agents/tools/web-search.ts",
"hashed_secret": "527ee41f36386e85fa932ef09471ca017f3c95c8", "hashed_secret": "527ee41f36386e85fa932ef09471ca017f3c95c8",
"is_verified": false, "is_verified": false,
"line_number": 199 "line_number": 207
} }
], ],
"src/agents/tools/web-tools.enabled-defaults.test.ts": [ "src/agents/tools/web-tools.enabled-defaults.test.ts": [
@ -1476,14 +1621,14 @@
"filename": "src/agents/tools/web-tools.enabled-defaults.test.ts", "filename": "src/agents/tools/web-tools.enabled-defaults.test.ts",
"hashed_secret": "47b249a75ca78fdb578d0f28c33685e27ea82684", "hashed_secret": "47b249a75ca78fdb578d0f28c33685e27ea82684",
"is_verified": false, "is_verified": false,
"line_number": 213 "line_number": 268
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/agents/tools/web-tools.enabled-defaults.test.ts", "filename": "src/agents/tools/web-tools.enabled-defaults.test.ts",
"hashed_secret": "d0ffd81d6d7ad1bc3c365660fe8882480c9a986e", "hashed_secret": "d0ffd81d6d7ad1bc3c365660fe8882480c9a986e",
"is_verified": false, "is_verified": false,
"line_number": 242 "line_number": 297
} }
], ],
"src/agents/tools/web-tools.fetch.test.ts": [ "src/agents/tools/web-tools.fetch.test.ts": [
@ -1533,7 +1678,7 @@
"filename": "src/auto-reply/status.test.ts", "filename": "src/auto-reply/status.test.ts",
"hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f",
"is_verified": false, "is_verified": false,
"line_number": 20 "line_number": 33
} }
], ],
"src/browser/cdp.helpers.test.ts": [ "src/browser/cdp.helpers.test.ts": [
@ -1563,6 +1708,15 @@
"line_number": 13 "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": [ "src/cli/update-cli.test.ts": [
{ {
"type": "Hex High Entropy String", "type": "Hex High Entropy String",
@ -1610,7 +1764,16 @@
"filename": "src/commands/configure.gateway-auth.test.ts", "filename": "src/commands/configure.gateway-auth.test.ts",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false, "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": [ "src/commands/models/list.status.test.ts": [
@ -1771,41 +1934,50 @@
"line_number": 228 "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": [ "src/config/schema.ts": [
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/config/schema.ts", "filename": "src/config/schema.ts",
"hashed_secret": "e73c9fcad85cd4eecc74181ec4bdb31064d68439", "hashed_secret": "e73c9fcad85cd4eecc74181ec4bdb31064d68439",
"is_verified": false, "is_verified": false,
"line_number": 184 "line_number": 192
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/config/schema.ts", "filename": "src/config/schema.ts",
"hashed_secret": "2eda7cd978f39eebec3bf03e4410a40e14167fff", "hashed_secret": "2eda7cd978f39eebec3bf03e4410a40e14167fff",
"is_verified": false, "is_verified": false,
"line_number": 220 "line_number": 230
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/config/schema.ts", "filename": "src/config/schema.ts",
"hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208", "hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208",
"is_verified": false, "is_verified": false,
"line_number": 418 "line_number": 439
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/config/schema.ts", "filename": "src/config/schema.ts",
"hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae", "hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae",
"is_verified": false, "is_verified": false,
"line_number": 437 "line_number": 458
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/config/schema.ts", "filename": "src/config/schema.ts",
"hashed_secret": "bb7dfd9746e660e4a4374951ec5938ef0e343255", "hashed_secret": "bb7dfd9746e660e4a4374951ec5938ef0e343255",
"is_verified": false, "is_verified": false,
"line_number": 487 "line_number": 510
} }
], ],
"src/config/slack-http-config.test.ts": [ "src/config/slack-http-config.test.ts": [
@ -1905,14 +2077,14 @@
"filename": "src/gateway/server.auth.e2e.test.ts", "filename": "src/gateway/server.auth.e2e.test.ts",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false, "is_verified": false,
"line_number": 179 "line_number": 192
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/gateway/server.auth.e2e.test.ts", "filename": "src/gateway/server.auth.e2e.test.ts",
"hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c",
"is_verified": false, "is_verified": false,
"line_number": 197 "line_number": 210
} }
], ],
"src/gateway/session-utils.test.ts": [ "src/gateway/session-utils.test.ts": [
@ -1930,7 +2102,7 @@
"filename": "src/gateway/tools-invoke-http.test.ts", "filename": "src/gateway/tools-invoke-http.test.ts",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false, "is_verified": false,
"line_number": 56 "line_number": 133
} }
], ],
"src/gateway/ws-log.test.ts": [ "src/gateway/ws-log.test.ts": [
@ -1971,7 +2143,7 @@
"filename": "src/infra/outbound/message-action-runner.test.ts", "filename": "src/infra/outbound/message-action-runner.test.ts",
"hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc", "hashed_secret": "789cbe0407840b1c2041cb33452ff60f19bf58cc",
"is_verified": false, "is_verified": false,
"line_number": 385 "line_number": 423
} }
], ],
"src/infra/outbound/outbound-policy.test.ts": [ "src/infra/outbound/outbound-policy.test.ts": [
@ -2006,6 +2178,79 @@
"line_number": 61 "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": [ "src/logging/redact.test.ts": [
{ {
"type": "Base64 High Entropy String", "type": "Base64 High Entropy String",
@ -2096,21 +2341,21 @@
"filename": "src/memory/embeddings.test.ts", "filename": "src/memory/embeddings.test.ts",
"hashed_secret": "a47110e348a3063541fb1f1f640d635d457181a0", "hashed_secret": "a47110e348a3063541fb1f1f640d635d457181a0",
"is_verified": false, "is_verified": false,
"line_number": 32 "line_number": 34
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/memory/embeddings.test.ts", "filename": "src/memory/embeddings.test.ts",
"hashed_secret": "c734e47630dda71619c696d88381f06f7511bd78", "hashed_secret": "c734e47630dda71619c696d88381f06f7511bd78",
"is_verified": false, "is_verified": false,
"line_number": 149 "line_number": 151
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/memory/embeddings.test.ts", "filename": "src/memory/embeddings.test.ts",
"hashed_secret": "56e1d57b8db262b08bc73c60ed08d8c92e59503f", "hashed_secret": "56e1d57b8db262b08bc73c60ed08d8c92e59503f",
"is_verified": false, "is_verified": false,
"line_number": 179 "line_number": 181
} }
], ],
"src/pairing/pairing-store.ts": [ "src/pairing/pairing-store.ts": [
@ -2123,33 +2368,19 @@
} }
], ],
"src/security/audit.test.ts": [ "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", "type": "Secret Keyword",
"filename": "src/security/audit.test.ts", "filename": "src/security/audit.test.ts",
"hashed_secret": "21f688ab56f76a99e5c6ed342291422f4e57e47f", "hashed_secret": "21f688ab56f76a99e5c6ed342291422f4e57e47f",
"is_verified": false, "is_verified": false,
"line_number": 1046 "line_number": 1187
}, },
{ {
"type": "Secret Keyword", "type": "Secret Keyword",
"filename": "src/security/audit.test.ts", "filename": "src/security/audit.test.ts",
"hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e", "hashed_secret": "3dc927d80543dc0f643940b70d066bd4b4c4b78e",
"is_verified": false, "is_verified": false,
"line_number": 1077 "line_number": 1218
} }
], ],
"src/tts/tts.test.ts": [ "src/tts/tts.test.ts": [
@ -2165,7 +2396,28 @@
"filename": "src/tts/tts.test.ts", "filename": "src/tts/tts.test.ts",
"hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb", "hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb",
"is_verified": false, "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": [ "src/web/qr-image.test.ts": [
@ -2185,7 +2437,16 @@
"is_verified": false, "is_verified": false,
"line_number": 182 "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"
} }

View File

@ -3,6 +3,7 @@ package bot.molt.android
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.location.LocationManager import android.location.LocationManager
import android.os.Build import android.os.Build
import android.os.SystemClock import android.os.SystemClock
@ -257,6 +258,36 @@ class NodeRuntime(context: Context) {
return if (trimmed.isEmpty()) "main" else trimmed 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() { private fun maybeNavigateToA2uiOnConnect() {
val a2uiUrl = resolveA2uiHostUrl() ?: return val a2uiUrl = resolveA2uiHostUrl() ?: return
val current = canvas.currentUrl()?.trim().orEmpty() val current = canvas.currentUrl()?.trim().orEmpty()
@ -653,6 +684,11 @@ class NodeRuntime(context: Context) {
scope.launch { scope.launch {
val trimmed = payloadJson.trim() val trimmed = payloadJson.trim()
if (trimmed.isEmpty()) return@launch 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 = val root =
try { try {

View File

@ -5,6 +5,7 @@ import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.CertificateException import java.security.cert.CertificateException
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.HostnameVerifier import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory import javax.net.ssl.SSLSocketFactory
@ -62,7 +63,18 @@ fun buildGatewayTlsConfig(
return GatewayTlsConfig( return GatewayTlsConfig(
sslSocketFactory = context.socketFactory, sslSocketFactory = context.socketFactory,
trustManager = trustManager, 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)
},
) )
} }

View File

@ -326,7 +326,15 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
// Some embedded web UIs (incl. the "background website") use localStorage/sessionStorage. // Some embedded web UIs (incl. the "background website") use localStorage/sessionStorage.
settings.domStorageEnabled = true 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)) { if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false) WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
} else { } else {

View File

@ -634,18 +634,27 @@ extension GatewayEndpointStore {
components.scheme = "http" components.scheme = "http"
} }
components.path = "/" 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), if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines),
!token.isEmpty !token.isEmpty
{ {
queryItems.append(URLQueryItem(name: "token", value: token)) fragmentItems.append(URLQueryItem(name: "token", value: token))
} }
if let password = config.password?.trimmingCharacters(in: .whitespacesAndNewlines), if let password = config.password?.trimmingCharacters(in: .whitespacesAndNewlines),
!password.isEmpty !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 { guard let url = components.url else {
throw NSError(domain: "Dashboard", code: 2, userInfo: [ throw NSError(domain: "Dashboard", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Failed to build dashboard URL", NSLocalizedDescriptionKey: "Failed to build dashboard URL",

View File

@ -323,13 +323,13 @@ The wizard now opens your browser with a tokenized dashboard URL right after onb
**Localhost (same machine):** **Localhost (same machine):**
- Open `http://127.0.0.1:18789/`. - 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. - 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:** **Not on localhost:**
- **Tailscale Serve** (recommended): keep bind loopback, run `moltbot gateway --tailscale serve`, open `https://<magicdns>/`. If `gateway.auth.allowTailscale` is `true`, identity headers satisfy auth (no token). - **Tailscale Serve** (recommended): keep bind loopback, run `moltbot gateway --tailscale serve`, open `https://<magicdns>/`. If `gateway.auth.allowTailscale` is `true`, identity headers satisfy auth (no token).
- **Tailnet bind**: run `moltbot gateway --bind tailnet --token "<token>"`, open `http://<tailscale-ip>:18789/`, paste token in dashboard settings. - **Tailnet bind**: run `moltbot gateway --bind tailnet --token "<token>"`, open `http://<tailscale-ip>: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. 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): Facts (from code):
- The Control UI stores the token in browser localStorage key `moltbot.control.settings.v1`. - 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: Fix:
- Fastest: `moltbot dashboard` (prints + copies tokenized link, tries to open; shows SSH hint if headless). - Fastest: `moltbot dashboard` (prints + copies tokenized link, tries to open; shows SSH hint if headless).
- If you dont have a token yet: `moltbot doctor --generate-gateway-token`. - If you dont 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. - 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. - 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 ### I set gatewaybind tailnet but it cant bind nothing listens

View File

@ -103,7 +103,7 @@ server {
## 5) Access Moltbot and grant privileges ## 5) Access Moltbot and grant privileges
Access `https://<vm-name>.exe.xyz/?token=YOUR-TOKEN-FROM-TERMINAL`. Approve Access `https://<vm-name>.exe.xyz/#token=YOUR-TOKEN-FROM-TERMINAL`. Approve
devices with `moltbot devices list` and `moltbot device approve`. When in doubt, devices with `moltbot devices list` and `moltbot device approve`. When in doubt,
use Shelley from your browser! use Shelley from your browser!

View File

@ -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. - 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). - 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) ## 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. - **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). - **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 ## If you see “unauthorized” / 1008
- Run `moltbot dashboard` to get a fresh tokenized link. - 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`). - In the dashboard settings, paste the same token you configured in `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).

View File

@ -186,7 +186,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"file-type": "^21.3.0", "file-type": "^21.3.0",
"grammy": "^1.39.3", "grammy": "^1.39.3",
"hono": "4.11.4", "hono": "4.11.7",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
@ -201,7 +201,7 @@
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"sqlite-vec": "0.1.7-alpha.2", "sqlite-vec": "0.1.7-alpha.2",
"tar": "7.5.4", "tar": "7.5.7",
"tslog": "^4.10.2", "tslog": "^4.10.2",
"undici": "^7.19.0", "undici": "^7.19.0",
"ws": "^8.19.0", "ws": "^8.19.0",
@ -242,14 +242,20 @@
"wireit": "^0.14.12" "wireit": "^0.14.12"
}, },
"overrides": { "overrides": {
"tar": "7.5.4" "tar": "7.5.7",
"form-data": "2.5.4",
"qs": "6.14.1",
"tough-cookie": "4.1.3"
}, },
"pnpm": { "pnpm": {
"minimumReleaseAge": 2880, "minimumReleaseAge": 2880,
"overrides": { "overrides": {
"@sinclair/typebox": "0.34.47", "@sinclair/typebox": "0.34.47",
"hono": "4.11.4", "hono": "4.11.7",
"tar": "7.5.4" "tar": "7.5.7",
"form-data": "2.5.4",
"qs": "6.14.1",
"tough-cookie": "4.1.3"
} }
}, },
"vitest": { "vitest": {

217
pnpm-lock.yaml generated
View File

@ -6,8 +6,11 @@ settings:
overrides: overrides:
'@sinclair/typebox': 0.34.47 '@sinclair/typebox': 0.34.47
hono: 4.11.4 hono: 4.11.7
tar: 7.5.4 tar: 7.5.7
form-data: 2.5.4
qs: 6.14.1
tough-cookie: 4.1.3
importers: importers:
@ -21,7 +24,7 @@ importers:
version: 3.975.0 version: 3.975.0
'@buape/carbon': '@buape/carbon':
specifier: 0.14.0 specifier: 0.14.0
version: 0.14.0(hono@4.11.4) version: 0.14.0(hono@4.11.7)
'@clack/prompts': '@clack/prompts':
specifier: ^0.11.0 specifier: ^0.11.0
version: 0.11.0 version: 0.11.0
@ -110,8 +113,8 @@ importers:
specifier: ^1.39.3 specifier: ^1.39.3
version: 1.39.3 version: 1.39.3
hono: hono:
specifier: 4.11.4 specifier: 4.11.7
version: 4.11.4 version: 4.11.7
jiti: jiti:
specifier: ^2.6.1 specifier: ^2.6.1
version: 2.6.1 version: 2.6.1
@ -155,8 +158,8 @@ importers:
specifier: 0.1.7-alpha.2 specifier: 0.1.7-alpha.2
version: 0.1.7-alpha.2 version: 0.1.7-alpha.2
tar: tar:
specifier: 7.5.4 specifier: 7.5.7
version: 7.5.4 version: 7.5.7
tslog: tslog:
specifier: ^4.10.2 specifier: ^4.10.2
version: 4.10.2 version: 4.10.2
@ -383,12 +386,12 @@ importers:
'@microsoft/agents-hosting-extensions-teams': '@microsoft/agents-hosting-extensions-teams':
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.2 version: 1.2.2
moltbot:
specifier: workspace:*
version: link:../..
express: express:
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1 version: 5.2.1
moltbot:
specifier: workspace:*
version: link:../..
proper-lockfile: proper-lockfile:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
@ -1090,7 +1093,7 @@ packages:
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
engines: {node: '>=18.14.1'} engines: {node: '>=18.14.1'}
peerDependencies: peerDependencies:
hono: 4.11.4 hono: 4.11.7
'@huggingface/jinja@0.5.3': '@huggingface/jinja@0.5.3':
resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==} resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==}
@ -3214,11 +3217,6 @@ packages:
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} 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: cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -3632,17 +3630,10 @@ packages:
forever-agent@0.6.1: forever-agent@0.6.1:
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
form-data@2.3.3: form-data@2.5.4:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} resolution: {integrity: sha512-Y/3MmRiR8Nd+0CUtrbvcKtKzLWiUfpQ7DFVggH8PwmGt/0r7RSy32GuP4hpCJlQNEBusisSx1DLtD8uD386HJQ==}
engines: {node: '>= 0.12'} engines: {node: '>= 0.12'}
deprecated: This version has an incorrect dependency; please use v2.5.5
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'}
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
@ -3764,6 +3755,10 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} 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: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3793,8 +3788,8 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
hono@4.11.4: hono@4.11.7:
resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
hookified@1.15.0: hookified@1.15.0:
@ -4809,9 +4804,8 @@ packages:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
qs@6.5.3: querystringify@2.2.0:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
engines: {node: '>=0.6'}
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -4894,6 +4888,9 @@ packages:
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} 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: resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@ -5190,8 +5187,8 @@ packages:
tailwindcss@4.1.17: tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
tar@7.5.4: tar@7.5.7:
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
thenify-all@1.6.0: thenify-all@1.6.0:
@ -5246,9 +5243,9 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
tough-cookie@2.5.0: tough-cookie@4.1.3:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
engines: {node: '>=0.8'} engines: {node: '>=6'}
tr46@0.0.3: tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@ -5334,6 +5331,10 @@ packages:
universal-user-agent@7.0.3: universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} 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: universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -5351,6 +5352,9 @@ packages:
url-join@4.0.1: url-join@4.0.1:
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -6433,14 +6437,14 @@ snapshots:
'@borewit/text-codec@0.2.1': {} '@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: dependencies:
'@types/node': 25.0.10 '@types/node': 25.0.10
discord-api-types: 0.38.37 discord-api-types: 0.38.37
optionalDependencies: optionalDependencies:
'@cloudflare/workers-types': 4.20260120.0 '@cloudflare/workers-types': 4.20260120.0
'@discordjs/voice': 0.19.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/bun': 1.3.6
'@types/ws': 8.18.1 '@types/ws': 8.18.1
ws: 8.19.0 ws: 8.19.0
@ -6696,9 +6700,9 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@hono/node-server@1.19.9(hono@4.11.4)': '@hono/node-server@1.19.9(hono@4.11.7)':
dependencies: dependencies:
hono: 4.11.4 hono: 4.11.7
optional: true optional: true
'@huggingface/jinja@0.5.3': '@huggingface/jinja@0.5.3':
@ -7973,7 +7977,7 @@ snapshots:
'@types/retry': 0.12.0 '@types/retry': 0.12.0
axios: 1.13.2(debug@4.4.3) axios: 1.13.2(debug@4.4.3)
eventemitter3: 5.0.4 eventemitter3: 5.0.4
form-data: 4.0.5 form-data: 2.5.4
is-electron: 2.2.2 is-electron: 2.2.2
is-stream: 2.0.1 is-stream: 2.0.1
p-queue: 6.6.2 p-queue: 6.6.2
@ -8543,7 +8547,7 @@ snapshots:
'@types/caseless': 0.12.5 '@types/caseless': 0.12.5
'@types/node': 25.0.10 '@types/node': 25.0.10
'@types/tough-cookie': 4.0.5 '@types/tough-cookie': 4.0.5
form-data: 2.5.5 form-data: 2.5.4
'@types/retry@0.12.0': {} '@types/retry@0.12.0': {}
@ -8932,7 +8936,7 @@ snapshots:
axios@1.13.2(debug@4.4.3): axios@1.13.2(debug@4.4.3):
dependencies: dependencies:
follow-redirects: 1.15.11(debug@4.4.3) 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 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
@ -9098,84 +9102,6 @@ snapshots:
dependencies: dependencies:
clsx: 2.1.1 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: cli-cursor@5.0.0:
dependencies: dependencies:
restore-cursor: 5.1.0 restore-cursor: 5.1.0
@ -9217,7 +9143,7 @@ snapshots:
npmlog: 6.0.2 npmlog: 6.0.2
rc: 1.2.8 rc: 1.2.8
semver: 7.7.3 semver: 7.7.3
tar: 7.5.4 tar: 7.5.7
url-join: 4.0.1 url-join: 4.0.1
which: 2.0.2 which: 2.0.2
yargs: 17.7.2 yargs: 17.7.2
@ -9650,29 +9576,15 @@ snapshots:
forever-agent@0.6.1: {} forever-agent@0.6.1: {}
form-data@2.3.3: form-data@2.5.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@2.5.5:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
combined-stream: 1.0.8 combined-stream: 1.0.8
es-set-tostringtag: 2.1.0 es-set-tostringtag: 2.1.0
hasown: 2.0.2 has-own: 1.0.1
mime-types: 2.1.35 mime-types: 2.1.35
safe-buffer: 5.2.1 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: formdata-polyfill@4.0.10:
dependencies: dependencies:
fetch-blob: 3.2.0 fetch-blob: 3.2.0
@ -9825,6 +9737,8 @@ snapshots:
has-flag@4.0.0: {} has-flag@4.0.0: {}
has-own@1.0.1: {}
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2: has-tostringtag@1.0.2:
@ -9851,7 +9765,7 @@ snapshots:
highlight.js@11.11.1: {} highlight.js@11.11.1: {}
hono@4.11.4: {} hono@4.11.7: {}
hookified@1.15.0: {} hookified@1.15.0: {}
@ -10930,7 +10844,7 @@ snapshots:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0
qs@6.5.3: {} querystringify@2.2.0: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
@ -11025,7 +10939,7 @@ snapshots:
request: 2.88.2 request: 2.88.2
request-promise-core: 1.1.4(request@2.88.2) request-promise-core: 1.1.4(request@2.88.2)
stealthy-require: 1.1.1 stealthy-require: 1.1.1
tough-cookie: 2.5.0 tough-cookie: 4.1.3
request@2.88.2: request@2.88.2:
dependencies: dependencies:
@ -11035,7 +10949,7 @@ snapshots:
combined-stream: 1.0.8 combined-stream: 1.0.8
extend: 3.0.2 extend: 3.0.2
forever-agent: 0.6.1 forever-agent: 0.6.1
form-data: 2.3.3 form-data: 2.5.4
har-validator: 5.1.5 har-validator: 5.1.5
http-signature: 1.2.0 http-signature: 1.2.0
is-typedarray: 1.0.0 is-typedarray: 1.0.0
@ -11044,9 +10958,9 @@ snapshots:
mime-types: 2.1.35 mime-types: 2.1.35
oauth-sign: 0.9.0 oauth-sign: 0.9.0
performance-now: 2.1.0 performance-now: 2.1.0
qs: 6.5.3 qs: 6.14.1
safe-buffer: 5.2.1 safe-buffer: 5.2.1
tough-cookie: 2.5.0 tough-cookie: 4.1.3
tunnel-agent: 0.6.0 tunnel-agent: 0.6.0
uuid: 3.4.0 uuid: 3.4.0
@ -11061,6 +10975,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
requires-port@1.0.0: {}
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}
restore-cursor@5.1.0: restore-cursor@5.1.0:
@ -11470,7 +11386,7 @@ snapshots:
tailwindcss@4.1.17: {} tailwindcss@4.1.17: {}
tar@7.5.4: tar@7.5.7:
dependencies: dependencies:
'@isaacs/fs-minipass': 4.0.1 '@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0 chownr: 3.0.0
@ -11522,10 +11438,12 @@ snapshots:
totalist@3.0.1: {} totalist@3.0.1: {}
tough-cookie@2.5.0: tough-cookie@4.1.3:
dependencies: dependencies:
psl: 1.15.0 psl: 1.15.0
punycode: 2.3.1 punycode: 2.3.1
universalify: 0.2.0
url-parse: 1.5.10
tr46@0.0.3: {} tr46@0.0.3: {}
@ -11599,6 +11517,8 @@ snapshots:
universal-user-agent@7.0.3: universal-user-agent@7.0.3:
optional: true optional: true
universalify@0.2.0: {}
universalify@2.0.1: universalify@2.0.1:
optional: true optional: true
@ -11613,6 +11533,11 @@ snapshots:
url-join@4.0.1: url-join@4.0.1:
optional: true optional: true
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
utils-merge@1.0.1: {} utils-merge@1.0.1: {}

View File

@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { dashboardCommand } from "./dashboard.js"; import { dashboardCommand } from "./dashboard.js";
import type { RuntimeEnv } from "../runtime.js";
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
readConfigFileSnapshot: vi.fn(), readConfigFileSnapshot: vi.fn(),
@ -31,8 +32,10 @@ vi.mock("../infra/clipboard.js", () => ({
const runtime = { const runtime = {
log: vi.fn(), log: vi.fn(),
error: vi.fn(), error: vi.fn(),
exit: vi.fn(), exit: vi.fn((_code?: number): never => {
}; throw new Error("exit");
}),
} satisfies RuntimeEnv;
function resetRuntime() { function resetRuntime() {
runtime.log.mockClear(); runtime.log.mockClear();
@ -84,8 +87,8 @@ describe("dashboardCommand", () => {
customBindHost: undefined, customBindHost: undefined,
basePath: undefined, basePath: undefined,
}); });
expect(mocks.copyToClipboard).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(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
expect(runtime.log).toHaveBeenCalledWith( expect(runtime.log).toHaveBeenCalledWith(
"Opened in your browser. Keep that tab to control Moltbot.", "Opened in your browser. Keep that tab to control Moltbot.",
); );

View File

@ -31,7 +31,14 @@ export async function dashboardCommand(
customBindHost, customBindHost,
basePath, 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}`); runtime.log(`Dashboard URL: ${authedUrl}`);

View File

@ -168,8 +168,14 @@ export function formatControlUiSshHint(params: {
const basePath = normalizeControlUiBasePath(params.basePath); const basePath = normalizeControlUiBasePath(params.basePath);
const uiPath = basePath ? `${basePath}/` : "/"; const uiPath = basePath ? `${basePath}/` : "/";
const localUrl = `http://localhost:${params.port}${uiPath}`; const localUrl = `http://localhost:${params.port}${uiPath}`;
const tokenParam = params.token ? `?token=${encodeURIComponent(params.token)}` : ""; const authedUrl = (() => {
const authedUrl = params.token ? `${localUrl}${tokenParam}` : undefined; 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(); const sshTarget = resolveSshTargetHint();
return [ return [
"No GUI detected. Open from your computer:", "No GUI detected. Open from your computer:",

View File

@ -202,6 +202,7 @@ const FIELD_LABELS: Record<string, string> = {
"gateway.controlUi.basePath": "Control UI Base Path", "gateway.controlUi.basePath": "Control UI Base Path",
"gateway.controlUi.allowInsecureAuth": "Allow Insecure Control UI Auth", "gateway.controlUi.allowInsecureAuth": "Allow Insecure Control UI Auth",
"gateway.controlUi.dangerouslyDisableDeviceAuth": "Dangerously Disable Control UI Device 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.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
"gateway.reload.mode": "Config Reload Mode", "gateway.reload.mode": "Config Reload Mode",
"gateway.reload.debounceMs": "Config Reload Debounce (ms)", "gateway.reload.debounceMs": "Config Reload Debounce (ms)",
@ -388,6 +389,8 @@ const FIELD_HELP: Record<string, string> = {
"Allow Control UI auth over insecure HTTP (token-only; not recommended).", "Allow Control UI auth over insecure HTTP (token-only; not recommended).",
"gateway.controlUi.dangerouslyDisableDeviceAuth": "gateway.controlUi.dangerouslyDisableDeviceAuth":
"DANGEROUS. Disable Control UI device identity checks (token/password only).", "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": "gateway.http.endpoints.chatCompletions.enabled":
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).", "Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).', "gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',

View File

@ -191,6 +191,19 @@ export type GatewayHttpConfig = {
endpoints?: GatewayHttpEndpointsConfig; 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 = { export type GatewayNodesConfig = {
/** Browser routing policy for node-hosted browser proxies. */ /** Browser routing policy for node-hosted browser proxies. */
browser?: { browser?: {
@ -232,6 +245,7 @@ export type GatewayConfig = {
reload?: GatewayReloadConfig; reload?: GatewayReloadConfig;
tls?: GatewayTlsConfig; tls?: GatewayTlsConfig;
http?: GatewayHttpConfig; http?: GatewayHttpConfig;
plugins?: GatewayPluginsConfig;
nodes?: GatewayNodesConfig; nodes?: GatewayNodesConfig;
/** /**
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection * IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection

View File

@ -426,6 +426,17 @@ export const MoltbotSchema = z
}) })
.strict() .strict()
.optional(), .optional(),
plugins: z
.object({
http: z
.object({
protectApiPaths: z.boolean().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
nodes: z nodes: z
.object({ .object({
browser: z browser: z

View File

@ -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 = export type ControlUiAvatarResolution =
| { kind: "none"; reason: string } | { kind: "none"; reason: string }
| { kind: "local"; filePath: string } | { kind: "local"; filePath: string }
@ -88,6 +102,7 @@ type ControlUiAvatarMeta = {
function sendJson(res: ServerResponse, status: number, body: unknown) { function sendJson(res: ServerResponse, status: number, body: unknown) {
res.statusCode = status; res.statusCode = status;
setControlUiSecurityHeaders(res);
res.setHeader("Content-Type", "application/json; charset=utf-8"); res.setHeader("Content-Type", "application/json; charset=utf-8");
res.setHeader("Cache-Control", "no-cache"); res.setHeader("Cache-Control", "no-cache");
res.end(JSON.stringify(body)); res.end(JSON.stringify(body));
@ -141,6 +156,7 @@ export function handleControlUiAvatarRequest(
if (req.method === "HEAD") { if (req.method === "HEAD") {
res.statusCode = 200; res.statusCode = 200;
setControlUiSecurityHeaders(res);
res.setHeader("Content-Type", contentTypeForExt(path.extname(resolved.filePath).toLowerCase())); res.setHeader("Content-Type", contentTypeForExt(path.extname(resolved.filePath).toLowerCase()));
res.setHeader("Cache-Control", "no-cache"); res.setHeader("Cache-Control", "no-cache");
res.end(); res.end();
@ -153,12 +169,14 @@ export function handleControlUiAvatarRequest(
function respondNotFound(res: ServerResponse) { function respondNotFound(res: ServerResponse) {
res.statusCode = 404; res.statusCode = 404;
setControlUiSecurityHeaders(res);
res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found"); res.end("Not Found");
} }
function serveFile(res: ServerResponse, filePath: string) { function serveFile(res: ServerResponse, filePath: string) {
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase();
setControlUiSecurityHeaders(res);
res.setHeader("Content-Type", contentTypeForExt(ext)); res.setHeader("Content-Type", contentTypeForExt(ext));
// Static UI should never be cached aggressively while iterating; allow the // Static UI should never be cached aggressively while iterating; allow the
// browser to revalidate. // browser to revalidate.
@ -214,6 +232,7 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex
agentId: resolvedAgentId, agentId: resolvedAgentId,
basePath, basePath,
}) ?? identity.avatar; }) ?? identity.avatar;
setControlUiSecurityHeaders(res);
res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "no-cache"); res.setHeader("Cache-Control", "no-cache");
const raw = fs.readFileSync(indexPath, "utf8"); const raw = fs.readFileSync(indexPath, "utf8");

View File

@ -102,6 +102,9 @@ export async function createGatewayRuntimeState(params: {
const handlePluginRequest = createGatewayPluginRequestHandler({ const handlePluginRequest = createGatewayPluginRequestHandler({
registry: params.pluginRegistry, registry: params.pluginRegistry,
log: params.logPlugins, 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); const bindHosts = await resolveGatewayListenHosts(params.bindHost);

View File

@ -2,6 +2,10 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import type { createSubsystemLogger } from "../../logging/subsystem.js"; import type { createSubsystemLogger } from "../../logging/subsystem.js";
import type { PluginRegistry } from "../../plugins/registry.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<typeof createSubsystemLogger>; type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
@ -13,6 +17,9 @@ export type PluginHttpRequestHandler = (
export function createGatewayPluginRequestHandler(params: { export function createGatewayPluginRequestHandler(params: {
registry: PluginRegistry; registry: PluginRegistry;
log: SubsystemLogger; log: SubsystemLogger;
auth?: ResolvedGatewayAuth;
trustedProxies?: string[];
protectApiPaths?: boolean;
}): PluginHttpRequestHandler { }): PluginHttpRequestHandler {
const { registry, log } = params; const { registry, log } = params;
return async (req, res) => { return async (req, res) => {
@ -20,8 +27,30 @@ export function createGatewayPluginRequestHandler(params: {
const handlers = registry.httpHandlers ?? []; const handlers = registry.httpHandlers ?? [];
if (routes.length === 0 && handlers.length === 0) return false; 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) { if (routes.length > 0) {
const url = new URL(req.url ?? "/", "http://localhost");
const route = routes.find((entry) => entry.path === url.pathname); const route = routes.find((entry) => entry.path === url.pathname);
if (route) { if (route) {
try { try {

View File

@ -101,7 +101,32 @@ export async function extractArchive(params: {
const label = kind === "zip" ? "extract zip" : "extract tar"; const label = kind === "zip" ? "extract zip" : "extract tar";
if (kind === "tar") { if (kind === "tar") {
await withTimeout( 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, params.timeoutMs,
label, label,
); );

View File

@ -249,11 +249,14 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
customBindHost: settings.customBindHost, customBindHost: settings.customBindHost,
basePath: controlUiBasePath, basePath: controlUiBasePath,
}); });
const tokenParam = const authedUrl = (() => {
settings.authMode === "token" && settings.gatewayToken if (settings.authMode !== "token" || !settings.gatewayToken) return "";
? `?token=${encodeURIComponent(settings.gatewayToken)}` const url = new URL(links.httpUrl);
: ""; const frag = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
const authedUrl = `${links.httpUrl}${tokenParam}`; frag.set("token", settings.gatewayToken);
url.hash = frag.toString();
return url.toString();
})();
const gatewayProbe = await probeGatewayReachable({ const gatewayProbe = await probeGatewayReachable({
url: links.wsUrl, url: links.wsUrl,
token: settings.authMode === "token" ? settings.gatewayToken : undefined, token: settings.authMode === "token" ? settings.gatewayToken : undefined,
@ -274,7 +277,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
await prompter.note( await prompter.note(
[ [
`Web UI: ${links.httpUrl}`, `Web UI: ${links.httpUrl}`,
tokenParam ? `Web UI (with token): ${authedUrl}` : undefined, authedUrl ? `Web UI (with token): ${authedUrl}` : undefined,
`Gateway WS: ${links.wsUrl}`, `Gateway WS: ${links.wsUrl}`,
gatewayStatusLine, gatewayStatusLine,
"Docs: https://docs.molt.bot/web/control-ui", "Docs: https://docs.molt.bot/web/control-ui",

View File

@ -115,10 +115,12 @@ export async function handleNostrProfileSave(host: MoltbotApp) {
}; };
try { try {
const auth = host.settings.token.trim() ? host.settings.token.trim() : host.password.trim();
const response = await fetch(buildNostrProfileUrl(accountId), { const response = await fetch(buildNostrProfileUrl(accountId), {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
}, },
body: JSON.stringify(state.values), body: JSON.stringify(state.values),
}); });
@ -180,10 +182,12 @@ export async function handleNostrProfileImport(host: MoltbotApp) {
}; };
try { try {
const auth = host.settings.token.trim() ? host.settings.token.trim() : host.password.trim();
const response = await fetch(buildNostrProfileUrl(accountId, "/import"), { const response = await fetch(buildNostrProfileUrl(accountId, "/import"), {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
}, },
body: JSON.stringify({ autoMerge: true }), body: JSON.stringify({ autoMerge: true }),
}); });

View File

@ -58,12 +58,24 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) {
} }
export function applySettingsFromUrl(host: SettingsHost) { export function applySettingsFromUrl(host: SettingsHost) {
if (!window.location.search) return; const hasQuery = Boolean(window.location.search);
const params = new URLSearchParams(window.location.search); const hasHash = Boolean(window.location.hash);
const tokenRaw = params.get("token"); if (!hasQuery && !hasHash) return;
const passwordRaw = params.get("password");
const sessionRaw = params.get("session"); const url = new URL(window.location.href);
const gatewayUrlRaw = params.get("gatewayUrl"); 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; let shouldCleanUrl = false;
if (tokenRaw != null) { if (tokenRaw != null) {
@ -71,7 +83,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
if (token && token !== host.settings.token) { if (token && token !== host.settings.token) {
applySettings(host, { ...host.settings, token }); applySettings(host, { ...host.settings, token });
} }
params.delete("token"); fragmentParams.delete("token");
shouldCleanUrl = true; shouldCleanUrl = true;
} }
@ -80,7 +92,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
if (password) { if (password) {
(host as { password: string }).password = password; (host as { password: string }).password = password;
} }
params.delete("password"); fragmentParams.delete("password");
shouldCleanUrl = true; shouldCleanUrl = true;
} }
@ -94,6 +106,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
lastActiveSessionKey: session, lastActiveSessionKey: session,
}); });
} }
// Keep session in the URL by default; don't force-clean it.
} }
if (gatewayUrlRaw != null) { if (gatewayUrlRaw != null) {
@ -101,13 +114,22 @@ export function applySettingsFromUrl(host: SettingsHost) {
if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) { if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {
host.pendingGatewayUrl = 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; shouldCleanUrl = true;
} }
if (!shouldCleanUrl) return; if (!shouldCleanUrl) return;
const url = new URL(window.location.href); url.search = queryParams.toString();
url.search = params.toString(); const nextFragment = fragmentParams.toString();
url.hash = nextFragment ? `#${nextFragment}` : "";
window.history.replaceState({}, "", url.toString()); window.history.replaceState({}, "", url.toString());
} }

View File

@ -149,34 +149,37 @@ describe("control UI routing", () => {
expect(container.scrollTop).toBe(maxScroll); expect(container.scrollTop).toBe(maxScroll);
}); });
it("hydrates token from URL params and strips it", async () => { it("hydrates token from URL fragment and strips it", async () => {
const app = mountApp("/ui/overview?token=abc123"); const app = mountApp("/ui/overview#token=abc123");
await app.updateComplete; await app.updateComplete;
expect(app.settings.token).toBe("abc123"); expect(app.settings.token).toBe("abc123");
expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.pathname).toBe("/ui/overview");
expect(window.location.search).toBe(""); expect(window.location.search).toBe("");
expect(window.location.hash).toBe("");
}); });
it("hydrates password from URL params and strips it", async () => { it("hydrates password from URL fragment and strips it", async () => {
const app = mountApp("/ui/overview?password=sekret"); const app = mountApp("/ui/overview#password=sekret");
await app.updateComplete; await app.updateComplete;
expect(app.password).toBe("sekret"); expect(app.password).toBe("sekret");
expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.pathname).toBe("/ui/overview");
expect(window.location.search).toBe(""); 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( localStorage.setItem(
"moltbot.control.settings.v1", "moltbot.control.settings.v1",
JSON.stringify({ token: "existing-token" }), JSON.stringify({ token: "existing-token" }),
); );
const app = mountApp("/ui/overview?token=abc123"); const app = mountApp("/ui/overview#token=abc123");
await app.updateComplete; await app.updateComplete;
expect(app.settings.token).toBe("abc123"); expect(app.settings.token).toBe("abc123");
expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.pathname).toBe("/ui/overview");
expect(window.location.search).toBe(""); expect(window.location.search).toBe("");
expect(window.location.hash).toBe("");
}); });
}); });