diff --git a/CHANGELOG.md b/CHANGELOG.md index e16c962a4a..17f957f07f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ Status: beta. - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. - macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. - CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0. +- Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam. ### Breaking - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). diff --git a/docs/cli/security.md b/docs/cli/security.md index 6621816164..551debc998 100644 --- a/docs/cli/security.md +++ b/docs/cli/security.md @@ -20,5 +20,5 @@ moltbot security audit --deep moltbot security audit --fix ``` -The audit warns when multiple DM senders share the main session and recommends `session.dmScope="per-channel-peer"` for shared inboxes. +The audit warns when multiple DM senders share the main session and recommends `session.dmScope="per-channel-peer"` (or `per-account-channel-peer` for multi-account channels) for shared inboxes. It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled. diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 58ac571455..b15b1a1eac 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -11,7 +11,8 @@ Use `session.dmScope` to control how **direct messages** are grouped: - `main` (default): all DMs share the main session for continuity. - `per-peer`: isolate by sender id across channels. - `per-channel-peer`: isolate by channel + sender (recommended for multi-user inboxes). -Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer` or `per-channel-peer`. +- `per-account-channel-peer`: isolate by account + channel + sender (recommended for multi-account inboxes). +Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-account-channel-peer`. ## Gateway is the source of truth All session state is **owned by the gateway** (the “master” Moltbot). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files. @@ -44,6 +45,7 @@ the workspace is writable. See [Memory](/concepts/memory) and - Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation. - `per-peer`: `agent::dm:`. - `per-channel-peer`: `agent:::dm:`. + - `per-account-channel-peer`: `agent::::dm:` (accountId defaults to `default`). - If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123`), the canonical key replaces `` so the same person shares a session across channels. - Group chats isolate state: `agent:::group:` (rooms/channels use `agent:::channel:`). - Telegram forum topics append `:topic:` to the group id for isolation. @@ -94,7 +96,7 @@ Send these as standalone messages so they register. { session: { scope: "per-sender", // keep group keys separate - dmScope: "main", // DM continuity (set per-channel-peer for shared inboxes) + dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes) identityLinks: { alice: ["telegram:123456789", "discord:987654321012345678"] }, diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 15261c809c..1d270974dc 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -2657,7 +2657,8 @@ Fields: - `main`: all DMs share the main session for continuity. - `per-peer`: isolate DMs by sender id across channels. - `per-channel-peer`: isolate DMs per channel + sender (recommended for multi-user inboxes). -- `identityLinks`: map canonical ids to provider-prefixed peers so the same person shares a DM session across channels when using `per-peer` or `per-channel-peer`. + - `per-account-channel-peer`: isolate DMs per account + channel + sender (recommended for multi-account inboxes). +- `identityLinks`: map canonical ids to provider-prefixed peers so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-account-channel-peer`. - Example: `alice: ["telegram:123456789", "discord:987654321012345678"]`. - `reset`: primary reset policy. Defaults to daily resets at 4:00 AM local time on the gateway host. - `mode`: `daily` or `idle` (default: `daily` when `reset` is present). diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index d29c3df485..a5d841c18f 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -199,7 +199,7 @@ By default, Moltbot routes **all DMs into the main session** so your assistant h } ``` -This prevents cross-user context leakage while keeping group chats isolated. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration). +This prevents cross-user context leakage while keeping group chats isolated. If you run multiple accounts on the same channel, use `per-account-channel-peer` instead. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration). ## Allowlists (DM + groups) — terminology diff --git a/src/commands/doctor-security.ts b/src/commands/doctor-security.ts index bf2c94da7b..856b18bfb2 100644 --- a/src/commands/doctor-security.ts +++ b/src/commands/doctor-security.ts @@ -124,7 +124,7 @@ export async function noteSecurityWarnings(cfg: MoltbotConfig) { if (dmScope === "main" && isMultiUserDm) { warnings.push( - `- ${params.label} DMs: multiple senders share the main session; set session.dmScope="per-channel-peer" to isolate sessions.`, + `- ${params.label} DMs: multiple senders share the main session; set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate sessions.`, ); } }; diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index e1f8dbe8e9..27ec07de4f 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -190,7 +190,7 @@ async function noteChannelPrimer( "DM security: default is pairing; unknown DMs get a pairing code.", `Approve with: ${formatCliCommand("moltbot pairing approve ")}`, 'Public DMs require dmPolicy="open" + allowFrom=["*"].', - 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', + 'Multi-user DMs: set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate sessions.', `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`, "", ...channelLines, @@ -238,7 +238,7 @@ async function maybeConfigureDmPolicies(params: { `Approve: ${formatCliCommand(`moltbot pairing approve ${policy.channel} `)}`, `Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`, `Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`, - 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.', + 'Multi-user DMs: set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate sessions.', `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`, ].join("\n"), `${policy.label} DM access`, diff --git a/src/config/schema.ts b/src/config/schema.ts index 9b5ad8be65..b4ec8723bb 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -591,7 +591,7 @@ const FIELD_HELP: Record = { "commands.restart": "Allow /restart and gateway restart tool actions (default: false).", "commands.useAccessGroups": "Enforce access-group allowlists/policies for commands.", "session.dmScope": - 'DM session scoping: "main" keeps continuity; "per-peer" or "per-channel-peer" isolates DM history (recommended for shared inboxes).', + 'DM session scoping: "main" keeps continuity; "per-peer", "per-channel-peer", or "per-account-channel-peer" isolates DM history (recommended for shared inboxes/multi-account).', "session.identityLinks": "Map canonical identities to provider-prefixed peer IDs for DM session linking (example: telegram:123456).", "channels.telegram.configWrites": diff --git a/src/security/audit.ts b/src/security/audit.ts index 7aebd69282..681d14c1d4 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -519,7 +519,8 @@ async function collectChannelSecurityFindings(params: { title: `${input.label} DMs share the main session`, detail: "Multiple DM senders currently share the main session, which can leak context across users.", - remediation: 'Set session.dmScope="per-channel-peer" to isolate DM sessions per sender.', + remediation: + 'Set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate DM sessions per sender.', }); } }; diff --git a/src/web/auto-reply/monitor/broadcast.ts b/src/web/auto-reply/monitor/broadcast.ts index ef76ce3b0b..c8f84a0480 100644 --- a/src/web/auto-reply/monitor/broadcast.ts +++ b/src/web/auto-reply/monitor/broadcast.ts @@ -54,11 +54,13 @@ export async function maybeBroadcastMessage(params: { sessionKey: buildAgentSessionKey({ agentId: normalizedAgentId, channel: "whatsapp", + accountId: params.route.accountId, peer: { kind: params.msg.chatType === "group" ? "group" : "dm", id: params.peerId, }, dmScope: params.cfg.session?.dmScope, + identityLinks: params.cfg.session?.identityLinks, }), mainSessionKey: buildAgentMainSessionKey({ agentId: normalizedAgentId,