diff --git a/CHANGELOG.md b/CHANGELOG.md index bb52cc8e6..379bfe7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Docs: https://docs.molt.bot Status: beta. ### Changes +- Gateway: fix device pairing rejection when local connection retries with existing non-silent pending request. Pending requests now upgrade to silent when a new local connection retries. +- Docs: add Docker config guidance for Control UI (controlUi.allowInsecureAuth to bypass device pairing for Docker bridge network connections). - Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope. - Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). @@ -13,7 +15,6 @@ Status: beta. - Branding: update launchd labels, mobile bundle IDs, and logging subsystems to bot.molt (legacy com.clawdbot migrations). Thanks @thewilloftheshadow. - Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt. - Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47. -- Memory Search: allow extra paths for memory indexing (ignores symlinks). (#3600) Thanks @kira-ariaki. - Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204. - Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281) - Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk. @@ -108,7 +109,6 @@ Status: beta. - Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne. - Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default. - Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers. -- Media: fix text attachment MIME misclassification with CSV/TSV inference and UTF-16 detection; add XML attribute escaping for file output. (#3628) Thanks @frankekn. - Build: align memory-core peer dependency with lockfile. - Security: add mDNS discovery mode with minimal default to reduce information disclosure. (#1882) Thanks @orlyjamie. - Security: harden URL fetches with DNS pinning to reduce rebinding risk. Thanks Chris Zheng. diff --git a/docker-compose.yml b/docker-compose.yml index 8ce610d6a..695969b64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,20 @@ services: moltbot-gateway: - image: ${CLAWDBOT_IMAGE:-moltbot:local} + image: ${MOLTBOT_IMAGE:-moltbot:local} environment: HOME: /home/node TERM: xterm-256color - CLAWDBOT_GATEWAY_TOKEN: ${CLAWDBOT_GATEWAY_TOKEN} + MOLTBOT_GATEWAY_TOKEN: ${MOLTBOT_GATEWAY_TOKEN} + MOLTBOT_SERVICE_TOKEN: ${MOLTBOT_SERVICE_TOKEN} CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} volumes: - - ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot - - ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd + - ${MOLTBOT_CONFIG_DIR:-~/.moltbot}:/home/node/.moltbot + - ${MOLTBOT_WORKSPACE_DIR:-~/clawd}:/home/node/clawd ports: - - "${CLAWDBOT_GATEWAY_PORT:-18789}:18789" - - "${CLAWDBOT_BRIDGE_PORT:-18790}:18790" + - "${MOLTBOT_GATEWAY_PORT:-18789}:18789" + - "${MOLTBOT_BRIDGE_PORT:-18790}:18790" init: true restart: unless-stopped command: @@ -22,13 +23,13 @@ services: "dist/index.js", "gateway", "--bind", - "${CLAWDBOT_GATEWAY_BIND:-lan}", + "${MOLTBOT_GATEWAY_BIND:-lan}", "--port", - "${CLAWDBOT_GATEWAY_PORT:-18789}" + "${MOLTBOT_GATEWAY_PORT:-18789}" ] moltbot-cli: - image: ${CLAWDBOT_IMAGE:-moltbot:local} + image: ${MOLTBOT_IMAGE:-moltbot:local} environment: HOME: /home/node TERM: xterm-256color @@ -37,8 +38,8 @@ services: CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} volumes: - - ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot - - ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd + - ${MOLTBOT_CONFIG_DIR}:/home/node/.moltbot + - ${MOLTBOT_WORKSPACE_DIR}:/home/node/moltbot stdin_open: true tty: true init: true diff --git a/docs/install/docker.md b/docs/install/docker.md index 8ca80e53b..c1b70e12c 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -56,6 +56,27 @@ It writes config/workspace on the host: - `~/.clawdbot/` - `~/clawd` +**Important for Docker**: The dashboard requires additional config to bypass device pairing when accessed via the Docker bridge network. Create `~/.moltbot/moltbot.json` (or `~/.clawdbot/moltbot.json`) with: + +```json +{ + "gateway": { + "mode": "local", + "auth": { + "mode": "token", + "token": "YOUR_GATEWAY_TOKEN_HERE" + }, + "controlUi": { + "allowInsecureAuth": true + } + } +} +``` + +Replace `YOUR_GATEWAY_TOKEN_HERE` with the actual token (or set `MOLTBOT_GATEWAY_TOKEN` environment variable instead). + +The `controlUi.allowInsecureAuth` setting allows token-only authentication for the Control UI, bypassing device identity pairing. This is necessary because connections from the Docker bridge network (e.g., `172.21.0.1`) are not detected as "local" connections, even when accessing via `http://127.0.0.1:18789`. Without this setting, you will see "pairing required" errors. + Running on a VPS? See [Hetzner (Docker VPS)](/platforms/hetzner). ### Manual flow (compose) diff --git a/src/infra/device-pairing.test.ts b/src/infra/device-pairing.test.ts index 5461498d9..4583ff116 100644 --- a/src/infra/device-pairing.test.ts +++ b/src/infra/device-pairing.test.ts @@ -41,4 +41,37 @@ describe("device pairing tokens", () => { paired = await getPairedDevice("device-1", baseDir); expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read"]); }); + + test("updates silent flag when local connection retries existing pending request", async () => { + const baseDir = await mkdtemp(join(tmpdir(), "moltbot-device-pairing-")); + + // First request from non-local connection (silent: false) + const firstRequest = await requestDevicePairing( + { + deviceId: "device-2", + publicKey: "public-key-2", + role: "operator", + scopes: [], + silent: false, + }, + baseDir, + ); + expect(firstRequest.created).toBe(true); + expect(firstRequest.request.silent).toBe(false); + + // Second request from local connection (silent: true) for same device + const secondRequest = await requestDevicePairing( + { + deviceId: "device-2", + publicKey: "public-key-2", + role: "operator", + scopes: [], + silent: true, + }, + baseDir, + ); + expect(secondRequest.created).toBe(false); + expect(secondRequest.request.silent).toBe(true); + expect(secondRequest.request.requestId).toBe(firstRequest.request.requestId); + }); }); diff --git a/src/infra/device-pairing.ts b/src/infra/device-pairing.ts index b190199eb..bef49adcc 100644 --- a/src/infra/device-pairing.ts +++ b/src/infra/device-pairing.ts @@ -246,6 +246,12 @@ export async function requestDevicePairing( } const existing = Object.values(state.pendingById).find((p) => p.deviceId === deviceId); if (existing) { + // If the new request is silent (local connection), update the existing request + // to allow auto-approval even if the original request was not silent. + if (req.silent && !existing.silent) { + existing.silent = true; + await persistState(state, baseDir); + } return { status: "pending", request: existing, created: false }; } const isRepair = Boolean(state.pairedByDeviceId[deviceId]);